diff --git a/include/Cache.h b/include/Cache.h index 76266ebd..b4dcdb90 100644 --- a/include/Cache.h +++ b/include/Cache.h @@ -286,6 +286,9 @@ public: bool isFormatValid(); void setCurrentFormat(); + //! Retrieve all the user ids from a room. + std::vector roomMembers(const std::string &room_id); + //! Check if the given user has power leve greater than than //! lowest power level of the given events. bool hasEnoughPowerLevel(const std::vector &eventTypes, @@ -358,8 +361,9 @@ public: void saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, mtx::crypto::OutboundGroupSessionPtr session); - OutboundGroupSessionDataRef getOutboundMegolmSession(const MegolmSessionIndex &index); - bool outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept; + 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); // // Inbound Megolm Sessions diff --git a/include/Olm.hpp b/include/Olm.hpp index 2f7b1d64..0839f01c 100644 --- a/include/Olm.hpp +++ b/include/Olm.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2"; @@ -62,4 +63,10 @@ void handle_pre_key_olm_message(const std::string &sender, const std::string &sender_key, const OlmCipherContent &content); + +mtx::events::msg::Encrypted +encrypt_group_message(const std::string &room_id, + const std::string &device_id, + const std::string &body); + } // namespace olm diff --git a/include/timeline/TimelineView.h b/include/timeline/TimelineView.h index 2c369d5f..5b5c2292 100644 --- a/include/timeline/TimelineView.h +++ b/include/timeline/TimelineView.h @@ -185,6 +185,7 @@ private: void sendRoomMessageHandler(const std::string &txn_id, const mtx::responses::EventId &res, mtx::http::RequestErr err); + void prepareEncryptedMessage(const PendingMessage &msg); //! Call the /messages endpoint to fill the timeline. void getMessages(); diff --git a/src/Cache.cc b/src/Cache.cc index 35ad8f9d..7c678b72 100644 --- a/src/Cache.cc +++ b/src/Cache.cc @@ -250,6 +250,36 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept session_storage.group_inbound_sessions.end(); } +void +Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index) +{ + using namespace mtx::crypto; + + if (!outboundMegolmSessionExists(room_id)) + return; + + OutboundGroupSessionData data; + OlmOutboundGroupSession *session; + { + std::unique_lock lock(session_storage.group_outbound_mtx); + data = session_storage.group_outbound_session_data[room_id]; + session = session_storage.group_outbound_sessions[room_id].get(); + + // Update with the current message. + data.message_index = message_index; + session_storage.group_outbound_session_data[room_id] = data; + } + + // Save the updated pickled data for the session. + json j; + j["data"] = data; + j["session"] = pickle(session, SECRET); + + auto txn = lmdb::txn::begin(env_); + lmdb::dbi_put(txn, outboundMegolmSessionDb_, lmdb::val(room_id), lmdb::val(j.dump())); + txn.commit(); +} + void Cache::saveOutboundMegolmSession(const std::string &room_id, const OutboundGroupSessionData &data, @@ -274,24 +304,21 @@ Cache::saveOutboundMegolmSession(const std::string &room_id, } bool -Cache::outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept +Cache::outboundMegolmSessionExists(const std::string &room_id) noexcept { - const auto key = index.to_hash(); - std::unique_lock lock(session_storage.group_outbound_mtx); - return (session_storage.group_outbound_sessions.find(key) != + return (session_storage.group_outbound_sessions.find(room_id) != session_storage.group_outbound_sessions.end()) && - (session_storage.group_outbound_session_data.find(key) != + (session_storage.group_outbound_session_data.find(room_id) != session_storage.group_outbound_session_data.end()); } OutboundGroupSessionDataRef -Cache::getOutboundMegolmSession(const MegolmSessionIndex &index) +Cache::getOutboundMegolmSession(const std::string &room_id) { - const auto key = index.to_hash(); std::unique_lock lock(session_storage.group_outbound_mtx); - return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[key].get(), - session_storage.group_outbound_session_data[key]}; + return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(), + session_storage.group_outbound_session_data[room_id]}; } void @@ -1537,6 +1564,26 @@ Cache::hasEnoughPowerLevel(const std::vector &eventTypes return user_level >= min_event_level; } +std::vector +Cache::roomMembers(const std::string &room_id) +{ + auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY); + + std::vector members; + std::string user_id, unused; + + auto db = getMembersDb(txn, room_id); + + auto cursor = lmdb::cursor::open(txn, db); + while (cursor.get(user_id, unused, MDB_NEXT)) + members.emplace_back(std::move(user_id)); + cursor.close(); + + txn.commit(); + + return members; +} + QHash Cache::DisplayNames; QHash Cache::AvatarUrls; diff --git a/src/Olm.cpp b/src/Olm.cpp index 769b0234..6e130277 100644 --- a/src/Olm.cpp +++ b/src/Olm.cpp @@ -136,4 +136,31 @@ handle_olm_normal_message(const std::string &, const std::string &, const OlmCip log::crypto()->warn("olm(1) not implemeted yet"); } +mtx::events::msg::Encrypted +encrypt_group_message(const std::string &room_id, + const std::string &device_id, + const std::string &body) +{ + using namespace mtx::events; + + // Always chech before for existence. + auto res = cache::client()->getOutboundMegolmSession(room_id); + auto payload = olm::client()->encrypt_group_message(res.session, body); + + // 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; + + auto message_index = olm_outbound_group_session_message_index(res.session); + log::crypto()->info("next message_index {}", message_index); + + // We need to re-pickle the session after we send a message to save the new message_index. + cache::client()->updateOutboundMegolmSession(room_id, message_index); + + return data; +} + } // namespace olm diff --git a/src/timeline/TimelineView.cc b/src/timeline/TimelineView.cc index 67da5dc6..9276a7bc 100644 --- a/src/timeline/TimelineView.cc +++ b/src/timeline/TimelineView.cc @@ -34,6 +34,19 @@ #include "timeline/widgets/ImageItem.h" #include "timeline/widgets/VideoItem.h" +class StateKeeper +{ +public: + StateKeeper(std::function &&fn) + : fn_(std::move(fn)) + {} + + ~StateKeeper() { fn_(); } + +private: + std::function fn_; +}; + using TimelineEvent = mtx::events::collections::TimelineEvents; DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent) @@ -329,6 +342,7 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEventinfo("decrypted data: \n {}", body.dump(2)); @@ -665,7 +679,7 @@ TimelineView::sendNextPendingMessage() log::main()->info("[{}] sending next queued message", m.txn_id); if (m.is_encrypted) { - // sendEncryptedMessage(m); + prepareEncryptedMessage(std::move(m)); log::main()->info("[{}] sending encrypted event", m.txn_id); return; } @@ -1124,3 +1138,236 @@ toRoomMessage(const PendingMessage &m) text.body = m.body.toStdString(); return text; } + +void +TimelineView::prepareEncryptedMessage(const PendingMessage &msg) +{ + const auto room_id = room_id_.toStdString(); + + using namespace mtx::events; + using namespace mtx::identifiers; + + json content; + + // Serialize the message to the plaintext that will be encrypted. + switch (msg.ty) { + case MessageType::Audio: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Emote: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::File: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Image: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Text: { + content = json(toRoomMessage(msg)); + break; + } + case MessageType::Video: { + content = json(toRoomMessage(msg)); + break; + } + default: + break; + } + + json doc{{"type", "m.room.message"}, {"content", content}, {"room_id", room_id}}; + + try { + // Check if we have already an outbound megolm session then we can use. + if (cache::client()->outboundMegolmSessionExists(room_id)) { + auto data = olm::encrypt_group_message( + room_id, http::v2::client()->device_id(), doc.dump()); + + http::v2::client() + ->send_room_message( + room_id, + msg.txn_id, + data, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + msg.txn_id, + std::placeholders::_1, + std::placeholders::_2)); + return; + } + + log::main()->info("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()); + + // TODO: needs to be moved in the lib. + auto megolm_payload = json{{"algorithm", "m.megolm.v1.aes-sha2"}, + {"room_id", room_id}, + {"session_id", session_id}, + {"session_key", session_key}}; + + // 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; // TODO Update me + cache::client()->saveOutboundMegolmSession( + room_id, session_data, std::move(outbound_session)); + + const auto members = cache::client()->roomMembers(room_id); + log::main()->info("retrieved {} members for {}", members.size(), room_id); + + auto keeper = std::make_shared( + [megolm_payload, room_id, doc, txn_id = msg.txn_id, this]() { + try { + auto data = olm::encrypt_group_message( + room_id, http::v2::client()->device_id(), doc.dump()); + + http::v2::client() + ->send_room_message( + room_id, + txn_id, + data, + std::bind(&TimelineView::sendRoomMessageHandler, + this, + txn_id, + std::placeholders::_1, + std::placeholders::_2)); + + } catch (const lmdb::error &e) { + log::db()->critical("failed to save megolm outbound session: {}", + e.what()); + } + }); + + mtx::requests::QueryKeys req; + for (const auto &member : members) + req.device_keys[member] = {}; + + http::v2::client()->query_keys( + req, + [keeper = std::move(keeper), megolm_payload](const mtx::responses::QueryKeys &res, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn("failed to query device keys: {} {}", + err->matrix_error.error, + static_cast(err->status_code)); + // TODO: Mark the event as failed. Communicate with the UI. + return; + } + + for (const auto &entry : res.device_keys) { + for (const auto &dev : entry.second) { + log::net()->info("received device {}", dev.first); + + const auto device_keys = dev.second.keys; + const auto curveKey = "curve25519:" + dev.first; + const auto edKey = "ed25519:" + dev.first; + + if ((device_keys.find(curveKey) == device_keys.end()) || + (device_keys.find(edKey) == device_keys.end())) { + log::net()->info( + "ignoring malformed keys for device {}", + dev.first); + continue; + } + + DevicePublicKeys pks; + pks.ed25519 = device_keys.at(edKey); + pks.curve25519 = device_keys.at(curveKey); + + // Validate signatures + for (const auto &algo : dev.second.keys) { + log::net()->info( + "dev keys {} {}", algo.first, algo.second); + } + + auto room_key = + olm::client() + ->create_room_key_event(UserId(dev.second.user_id), + pks.ed25519, + megolm_payload) + .dump(); + + http::v2::client()->claim_keys( + dev.second.user_id, + {dev.second.device_id}, + [keeper, + room_key, + pks, + user_id = dev.second.user_id, + device_id = dev.second.device_id]( + const mtx::responses::ClaimKeys &res, + mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "claim keys error: {}", + err->matrix_error.error); + return; + } + + log::net()->info("claimed keys for {} - {}", + user_id, + device_id); + + auto retrieved_devices = + res.one_time_keys.at(user_id); + for (const auto &rd : retrieved_devices) { + log::net()->info("{} : \n {}", + rd.first, + rd.second.dump(2)); + + // TODO: Verify signatures + auto otk = rd.second.begin()->at("key"); + auto id_key = pks.curve25519; + + auto session = + olm::client() + ->create_outbound_session(id_key, + otk); + + auto device_msg = + olm::client() + ->create_olm_encrypted_content( + session.get(), + room_key, + pks.curve25519); + + json body{ + {"messages", + {{user_id, + {{device_id, device_msg}}}}}}; + + http::v2::client()->send_to_device( + "m.room.encrypted", + body, + [keeper](mtx::http::RequestErr err) { + if (err) { + log::net()->warn( + "failed to send " + "send_to_device " + "message: {}", + err->matrix_error + .error); + } + }); + } + }); + } + } + }); + + } catch (const lmdb::error &e) { + log::db()->critical( + "failed to open outbound megolm session ({}): {}", room_id, e.what()); + return; + } +}