diff --git a/src/Cache.cc b/src/Cache.cc
index 35ad8f9d..7c678b72 100644
--- a/src/Cache.cc
+++ b/src/Cache.cc
@@ -251,6 +251,36 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept
}
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<std::mutex> 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<OutboundSessionObject>(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,
mtx::crypto::OutboundGroupSessionPtr session)
@@ -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<std::mutex> 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<std::mutex> 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<mtx::events::EventType> &eventTypes
return user_level >= min_event_level;
}
+std::vector<std::string>
+Cache::roomMembers(const std::string &room_id)
+{
+ auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+ std::vector<std::string> 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<QString, QString> Cache::DisplayNames;
QHash<QString, QString> 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<void()> &&fn)
+ : fn_(std::move(fn))
+ {}
+
+ ~StateKeeper() { fn_(); }
+
+private:
+ std::function<void()> fn_;
+};
+
using TimelineEvent = mtx::events::collections::TimelineEvents;
DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent)
@@ -329,6 +342,7 @@ TimelineView::parseEncryptedEvent(const mtx::events::EncryptedEvent<mtx::events:
body["event_id"] = e.event_id;
body["sender"] = e.sender;
body["origin_server_ts"] = e.origin_server_ts;
+ body["unsigned"] = e.unsigned_data;
log::crypto()->info("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<mtx::events::msg::Text>(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::Audio>(msg));
+ break;
+ }
+ case MessageType::Emote: {
+ content = json(toRoomMessage<msg::Emote>(msg));
+ break;
+ }
+ case MessageType::File: {
+ content = json(toRoomMessage<msg::File>(msg));
+ break;
+ }
+ case MessageType::Image: {
+ content = json(toRoomMessage<msg::Image>(msg));
+ break;
+ }
+ case MessageType::Text: {
+ content = json(toRoomMessage<msg::Text>(msg));
+ break;
+ }
+ case MessageType::Video: {
+ content = json(toRoomMessage<msg::Video>(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<msg::Encrypted, EventType::RoomEncrypted>(
+ 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<StateKeeper>(
+ [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<msg::Encrypted, EventType::RoomEncrypted>(
+ 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<int>(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;
+ }
+}
|