From 2ce129e6b61d18ac4a2e79702d3c1fd1302c6631 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 30 Nov 2020 00:26:27 +0100 Subject: [PATCH] Properly share and rotate sessions on member and device changes --- src/Cache.cpp | 87 ++++++++++++++-- src/Cache.h | 3 +- src/CacheCryptoStructs.h | 16 +++ src/Cache_p.h | 5 +- src/Olm.cpp | 183 +++++++++++++++++++++++++++++++-- src/timeline/TimelineModel.cpp | 82 ++------------- 6 files changed, 285 insertions(+), 91 deletions(-) diff --git a/src/Cache.cpp b/src/Cache.cpp index f113f716..97e99700 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -362,6 +362,7 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) void Cache::updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data_, mtx::crypto::OutboundGroupSessionPtr &ptr) { using namespace mtx::crypto; @@ -369,10 +370,10 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, if (!outboundMegolmSessionExists(room_id)) return; - OutboundGroupSessionData data; - data.message_index = olm_outbound_group_session_message_index(ptr.get()); - data.session_id = mtx::crypto::session_id(ptr.get()); - data.session_key = mtx::crypto::session_key(ptr.get()); + OutboundGroupSessionData data = data_; + data.message_index = olm_outbound_group_session_message_index(ptr.get()); + data.session_id = mtx::crypto::session_id(ptr.get()); + data.session_key = mtx::crypto::session_key(ptr.get()); // Save the updated pickled data for the session. json j; @@ -402,7 +403,7 @@ Cache::dropOutboundMegolmSession(const std::string &room_id) void Cache::saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session) + mtx::crypto::OutboundGroupSessionPtr &session) { using namespace mtx::crypto; const auto pickled = pickle(session.get(), SECRET); @@ -3095,6 +3096,39 @@ Cache::roomMembers(const std::string &room_id) return members; } +std::map> +Cache::getMembersWithKeys(const std::string &room_id) +{ + lmdb::val keys; + + try { + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + std::map> members; + + auto db = getMembersDb(txn, room_id); + auto keysDb = getUserKeysDb(txn); + + std::string user_id, unused; + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) { + auto res = lmdb::dbi_get(txn, keysDb, lmdb::val(user_id), keys); + + if (res) { + members[user_id] = + json::parse(std::string_view(keys.data(), keys.size())) + .get(); + } else { + members[user_id] = {}; + } + } + cursor.close(); + + return members; + } catch (std::exception &) { + return {}; + } +} + QString Cache::displayName(const QString &room_id, const QString &user_id) { @@ -3235,6 +3269,8 @@ Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::Query updates[user].self_signing_keys = keys; for (auto &[user, update] : updates) { + nhlog::db()->debug("Updated user keys: {}", user); + lmdb::val oldKeys; auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys); @@ -3297,6 +3333,8 @@ Cache::markUserKeysOutOfDate(lmdb::txn &txn, query.token = sync_token; for (const auto &user : user_ids) { + nhlog::db()->debug("Marking user keys out of date: {}", user); + lmdb::val oldKeys; auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys); @@ -3650,12 +3688,41 @@ from_json(const json &j, MemberInfo &info) info.avatar_url = j.at("avatar_url"); } +void +to_json(nlohmann::json &obj, const DeviceAndMasterKeys &msg) +{ + obj["devices"] = msg.devices; + obj["master_keys"] = msg.master_keys; +} + +void +from_json(const nlohmann::json &obj, DeviceAndMasterKeys &msg) +{ + msg.devices = obj.at("devices").get(); + msg.master_keys = obj.at("master_keys").get(); +} + +void +to_json(nlohmann::json &obj, const SharedWithUsers &msg) +{ + obj["keys"] = msg.keys; +} + +void +from_json(const nlohmann::json &obj, SharedWithUsers &msg) +{ + msg.keys = obj.at("keys").get>(); +} + void to_json(nlohmann::json &obj, const OutboundGroupSessionData &msg) { obj["session_id"] = msg.session_id; obj["session_key"] = msg.session_key; obj["message_index"] = msg.message_index; + + obj["initially"] = msg.initially; + obj["currently"] = msg.currently; } void @@ -3664,6 +3731,9 @@ from_json(const nlohmann::json &obj, OutboundGroupSessionData &msg) msg.session_id = obj.at("session_id"); msg.session_key = obj.at("session_key"); msg.message_index = obj.at("message_index"); + + msg.initially = obj.value("initially", SharedWithUsers{}); + msg.currently = obj.value("currently", SharedWithUsers{}); } void @@ -4098,9 +4168,9 @@ isRoomMember(const std::string &user_id, const std::string &room_id) void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session) + mtx::crypto::OutboundGroupSessionPtr &session) { - instance_->saveOutboundMegolmSession(room_id, data, std::move(session)); + instance_->saveOutboundMegolmSession(room_id, data, session); } OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id) @@ -4114,9 +4184,10 @@ outboundMegolmSessionExists(const std::string &room_id) noexcept } void updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session) { - instance_->updateOutboundMegolmSession(room_id, session); + instance_->updateOutboundMegolmSession(room_id, data, session); } void dropOutboundMegolmSession(const std::string &room_id) diff --git a/src/Cache.h b/src/Cache.h index 4418414d..f38f1960 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -235,13 +235,14 @@ isRoomMember(const std::string &user_id, const std::string &room_id); void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session); + mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h index 9f2cfe54..eb2cc445 100644 --- a/src/CacheCryptoStructs.h +++ b/src/CacheCryptoStructs.h @@ -6,12 +6,28 @@ #include #include +struct DeviceAndMasterKeys +{ + // map from device id or master key id to message_index + std::map devices, master_keys; +}; + +struct SharedWithUsers +{ + // userid to keys + std::map keys; +}; + // Extra information associated with an outbound megolm session. struct OutboundGroupSessionData { std::string session_id; std::string session_key; uint64_t message_index = 0; + + // who has access to this session. + // Rotate, when a user leaves the room and share, when a user gets added. + SharedWithUsers initially, currently; }; void diff --git a/src/Cache_p.h b/src/Cache_p.h index f8c4ceaf..fab2d964 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -59,6 +59,8 @@ public: // user cache stores user keys std::optional userKeys(const std::string &user_id); + std::map> getMembersWithKeys( + const std::string &room_id); void updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery); void markUserKeysOutOfDate(lmdb::txn &txn, @@ -232,10 +234,11 @@ public: // void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, - mtx::crypto::OutboundGroupSessionPtr session); + mtx::crypto::OutboundGroupSessionPtr &session); OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id); bool outboundMegolmSessionExists(const std::string &room_id) noexcept; void updateOutboundMegolmSession(const std::string &room_id, + const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr &session); void dropOutboundMegolmSession(const std::string &room_id); diff --git a/src/Olm.cpp b/src/Olm.cpp index 88e67159..c2200703 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -278,11 +278,168 @@ mtx::events::msg::Encrypted encrypt_group_message(const std::string &room_id, const std::string &device_id, nlohmann::json body) { using namespace mtx::events; + using namespace mtx::identifiers; + + auto own_user_id = http::client()->user_id().to_string(); + + auto members = cache::client()->getMembersWithKeys(room_id); + + std::map> sendSessionTo; + mtx::crypto::OutboundGroupSessionPtr session = nullptr; + OutboundGroupSessionData group_session_data; + + if (cache::outboundMegolmSessionExists(room_id)) { + auto res = cache::getOutboundMegolmSession(room_id); + + auto member_it = members.begin(); + auto session_member_it = res.data.currently.keys.begin(); + auto session_member_it_end = res.data.currently.keys.end(); + + while (member_it != members.end() || session_member_it != session_member_it_end) { + if (member_it == members.end()) { + // a member left, purge session! + nhlog::crypto()->debug( + "Rotating megolm session because of left member"); + break; + } + + if (session_member_it == session_member_it_end) { + // share with all remaining members + while (member_it != members.end()) { + sendSessionTo[member_it->first] = {}; + + if (member_it->second) + for (const auto &dev : + member_it->second->device_keys) + if (member_it->first != own_user_id || + dev.first != device_id) + sendSessionTo[member_it->first] + .push_back(dev.first); + + ++member_it; + } + + session = std::move(res.session); + break; + } + + if (member_it->first > session_member_it->first) { + // a member left, purge session + nhlog::crypto()->debug( + "Rotating megolm session because of left member"); + break; + } else if (member_it->first < session_member_it->first) { + // new member, send them the session at this index + sendSessionTo[member_it->first] = {}; + + for (const auto &dev : member_it->second->device_keys) + if (member_it->first != own_user_id || + dev.first != device_id) + sendSessionTo[member_it->first].push_back( + dev.first); + + ++member_it; + } else { + // compare devices + bool device_removed = false; + for (const auto &dev : session_member_it->second.devices) { + if (!member_it->second || + !member_it->second->device_keys.count(dev.first)) { + device_removed = true; + break; + } + } + + if (device_removed) { + // device removed, rotate session! + nhlog::crypto()->debug( + "Rotating megolm session because of removed device of {}", + member_it->first); + break; + } + + // check for new devices to share with + if (member_it->second) + for (const auto &dev : member_it->second->device_keys) + if (!session_member_it->second.devices.count( + dev.first) && + (member_it->first != own_user_id || + dev.first != device_id)) + sendSessionTo[member_it->first].push_back( + dev.first); + + ++member_it; + ++session_member_it; + if (member_it == members.end() && + session_member_it == session_member_it_end) { + // all devices match or are newly added + session = std::move(res.session); + } + } + } + + group_session_data = std::move(res.data); + } + + if (!session) { + nhlog::ui()->debug("creating new outbound megolm session"); + + // Create a new outbound megolm session. + session = olm::client()->init_outbound_group_session(); + const auto session_id = mtx::crypto::session_id(session.get()); + const auto session_key = mtx::crypto::session_key(session.get()); + + // Saving the new megolm session. + OutboundGroupSessionData session_data{}; + session_data.session_id = mtx::crypto::session_id(session.get()); + session_data.session_key = mtx::crypto::session_key(session.get()); + session_data.message_index = 0; + + sendSessionTo.clear(); + + for (const auto &[user, devices] : members) { + sendSessionTo[user] = {}; + session_data.initially.keys[user] = {}; + if (devices) { + for (const auto &[device_id_, key] : devices->device_keys) { + (void)key; + if (device_id != device_id_ || user != own_user_id) { + sendSessionTo[user].push_back(device_id_); + session_data.initially.keys[user] + .devices[device_id_] = 0; + } + } + } + } + + cache::saveOutboundMegolmSession(room_id, session_data, session); + group_session_data = std::move(session_data); + + { + 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)); + } + } + + mtx::events::DeviceEvent megolm_payload{}; + megolm_payload.content.algorithm = MEGOLM_ALGO; + megolm_payload.content.room_id = room_id; + megolm_payload.content.session_id = mtx::crypto::session_id(session.get()); + megolm_payload.content.session_key = mtx::crypto::session_key(session.get()); + megolm_payload.type = mtx::events::EventType::RoomKey; + + if (!sendSessionTo.empty()) + olm::send_encrypted_to_device_messages(sendSessionTo, megolm_payload); - // relations shouldn't be encrypted... mtx::common::ReplyRelatesTo relation; mtx::common::RelatesTo r_relation; + // relations shouldn't be encrypted... if (body["content"].contains("m.relates_to") && body["content"]["m.relates_to"].contains("m.in_reply_to")) { relation = body["content"]["m.relates_to"]; @@ -292,25 +449,35 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id, body["content"].erase("m.relates_to"); } - // Always check before for existence. - auto res = cache::getOutboundMegolmSession(room_id); - auto payload = olm::client()->encrypt_group_message(res.session.get(), body.dump()); + auto payload = olm::client()->encrypt_group_message(session.get(), body.dump()); // 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 = mtx::crypto::session_id(res.session.get()); + data.session_id = mtx::crypto::session_id(session.get()); data.device_id = device_id; data.algorithm = MEGOLM_ALGO; data.relates_to = relation; data.r_relates_to = r_relation; - res.data.message_index = olm_outbound_group_session_message_index(res.session.get()); - nhlog::crypto()->debug("next message_index {}", res.data.message_index); + group_session_data.message_index = olm_outbound_group_session_message_index(session.get()); + nhlog::crypto()->debug("next message_index {}", group_session_data.message_index); + + // update current set of members for the session with the new members and that message_index + for (const auto &[user, devices] : sendSessionTo) { + if (!group_session_data.currently.keys.count(user)) + group_session_data.currently.keys[user] = {}; + + for (const auto &device_id : devices) { + if (!group_session_data.currently.keys[user].devices.count(device_id)) + group_session_data.currently.keys[user].devices[device_id] = + group_session_data.message_index; + } + } // We need to re-pickle the session after we send a message to save the new message_index. - cache::updateOutboundMegolmSession(room_id, res.session); + cache::updateOutboundMegolmSession(room_id, group_session_data, session); return data; } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 53791c98..11fa60c0 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -910,80 +910,16 @@ TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent msg, mtx::events:: {"room_id", room_id}}; try { - // Check if we have already an outbound megolm session then we can use. - if (cache::outboundMegolmSessionExists(room_id)) { - mtx::events::EncryptedEvent event; - event.content = - olm::encrypt_group_message(room_id, http::client()->device_id(), doc); - 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; - event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); + mtx::events::EncryptedEvent event; + event.content = + olm::encrypt_group_message(room_id, http::client()->device_id(), doc); + 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; + event.origin_server_ts = QDateTime::currentMSecsSinceEpoch(); - emit this->addPendingMessageToStore(event); - return; - } - - nhlog::ui()->debug("creating new outbound megolm session"); - - // Create a new outbound megolm session. - auto outbound_session = olm::client()->init_outbound_group_session(); - const auto session_id = mtx::crypto::session_id(outbound_session.get()); - const auto session_key = mtx::crypto::session_key(outbound_session.get()); - - mtx::events::DeviceEvent megolm_payload; - megolm_payload.content.algorithm = "m.megolm.v1.aes-sha2"; - megolm_payload.content.room_id = room_id; - megolm_payload.content.session_id = session_id; - megolm_payload.content.session_key = session_key; - megolm_payload.type = mtx::events::EventType::RoomKey; - - // Saving the new megolm session. - // TODO: Maybe it's too early to save. - OutboundGroupSessionData session_data; - session_data.session_id = session_id; - session_data.session_key = session_key; - 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); - - std::map> targets; - for (const auto &member : members) - targets[member] = {}; - - olm::send_encrypted_to_device_messages(targets, megolm_payload); - - try { - mtx::events::EncryptedEvent event; - event.content = - olm::encrypt_group_message(room_id, http::client()->device_id(), doc); - 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; - 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); // TODO: Let the user know about the errors. } catch (const lmdb::error &e) {