Add support for sending encrypted messages
This commit is contained in:
parent
e5dd64c63a
commit
5d47cc3940
@ -286,6 +286,9 @@ public:
|
|||||||
bool isFormatValid();
|
bool isFormatValid();
|
||||||
void setCurrentFormat();
|
void setCurrentFormat();
|
||||||
|
|
||||||
|
//! Retrieve all the user ids from a room.
|
||||||
|
std::vector<std::string> roomMembers(const std::string &room_id);
|
||||||
|
|
||||||
//! Check if the given user has power leve greater than than
|
//! Check if the given user has power leve greater than than
|
||||||
//! lowest power level of the given events.
|
//! lowest power level of the given events.
|
||||||
bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
|
bool hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes,
|
||||||
@ -358,8 +361,9 @@ public:
|
|||||||
void saveOutboundMegolmSession(const std::string &room_id,
|
void saveOutboundMegolmSession(const std::string &room_id,
|
||||||
const OutboundGroupSessionData &data,
|
const OutboundGroupSessionData &data,
|
||||||
mtx::crypto::OutboundGroupSessionPtr session);
|
mtx::crypto::OutboundGroupSessionPtr session);
|
||||||
OutboundGroupSessionDataRef getOutboundMegolmSession(const MegolmSessionIndex &index);
|
OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
|
||||||
bool outboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept;
|
bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
|
||||||
|
void updateOutboundMegolmSession(const std::string &room_id, int message_index);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Inbound Megolm Sessions
|
// Inbound Megolm Sessions
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mtx.hpp>
|
||||||
#include <mtxclient/crypto/client.hpp>
|
#include <mtxclient/crypto/client.hpp>
|
||||||
|
|
||||||
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
|
constexpr auto OLM_ALGO = "m.olm.v1.curve25519-aes-sha2";
|
||||||
@ -62,4 +63,10 @@ void
|
|||||||
handle_pre_key_olm_message(const std::string &sender,
|
handle_pre_key_olm_message(const std::string &sender,
|
||||||
const std::string &sender_key,
|
const std::string &sender_key,
|
||||||
const OlmCipherContent &content);
|
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
|
} // namespace olm
|
||||||
|
@ -185,6 +185,7 @@ private:
|
|||||||
void sendRoomMessageHandler(const std::string &txn_id,
|
void sendRoomMessageHandler(const std::string &txn_id,
|
||||||
const mtx::responses::EventId &res,
|
const mtx::responses::EventId &res,
|
||||||
mtx::http::RequestErr err);
|
mtx::http::RequestErr err);
|
||||||
|
void prepareEncryptedMessage(const PendingMessage &msg);
|
||||||
|
|
||||||
//! Call the /messages endpoint to fill the timeline.
|
//! Call the /messages endpoint to fill the timeline.
|
||||||
void getMessages();
|
void getMessages();
|
||||||
|
65
src/Cache.cc
65
src/Cache.cc
@ -250,6 +250,36 @@ Cache::inboundMegolmSessionExists(const MegolmSessionIndex &index) noexcept
|
|||||||
session_storage.group_inbound_sessions.end();
|
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<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
|
void
|
||||||
Cache::saveOutboundMegolmSession(const std::string &room_id,
|
Cache::saveOutboundMegolmSession(const std::string &room_id,
|
||||||
const OutboundGroupSessionData &data,
|
const OutboundGroupSessionData &data,
|
||||||
@ -274,24 +304,21 @@ Cache::saveOutboundMegolmSession(const std::string &room_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
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);
|
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_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());
|
session_storage.group_outbound_session_data.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
OutboundGroupSessionDataRef
|
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);
|
std::unique_lock<std::mutex> lock(session_storage.group_outbound_mtx);
|
||||||
return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[key].get(),
|
return OutboundGroupSessionDataRef{session_storage.group_outbound_sessions[room_id].get(),
|
||||||
session_storage.group_outbound_session_data[key]};
|
session_storage.group_outbound_session_data[room_id]};
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@ -1537,6 +1564,26 @@ Cache::hasEnoughPowerLevel(const std::vector<mtx::events::EventType> &eventTypes
|
|||||||
return user_level >= min_event_level;
|
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::DisplayNames;
|
||||||
QHash<QString, QString> Cache::AvatarUrls;
|
QHash<QString, QString> Cache::AvatarUrls;
|
||||||
|
|
||||||
|
27
src/Olm.cpp
27
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");
|
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
|
} // namespace olm
|
||||||
|
@ -34,6 +34,19 @@
|
|||||||
#include "timeline/widgets/ImageItem.h"
|
#include "timeline/widgets/ImageItem.h"
|
||||||
#include "timeline/widgets/VideoItem.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;
|
using TimelineEvent = mtx::events::collections::TimelineEvents;
|
||||||
|
|
||||||
DateSeparator::DateSeparator(QDateTime datetime, QWidget *parent)
|
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["event_id"] = e.event_id;
|
||||||
body["sender"] = e.sender;
|
body["sender"] = e.sender;
|
||||||
body["origin_server_ts"] = e.origin_server_ts;
|
body["origin_server_ts"] = e.origin_server_ts;
|
||||||
|
body["unsigned"] = e.unsigned_data;
|
||||||
|
|
||||||
log::crypto()->info("decrypted data: \n {}", body.dump(2));
|
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);
|
log::main()->info("[{}] sending next queued message", m.txn_id);
|
||||||
|
|
||||||
if (m.is_encrypted) {
|
if (m.is_encrypted) {
|
||||||
// sendEncryptedMessage(m);
|
prepareEncryptedMessage(std::move(m));
|
||||||
log::main()->info("[{}] sending encrypted event", m.txn_id);
|
log::main()->info("[{}] sending encrypted event", m.txn_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1124,3 +1138,236 @@ toRoomMessage<mtx::events::msg::Text>(const PendingMessage &m)
|
|||||||
text.body = m.body.toStdString();
|
text.body = m.body.toStdString();
|
||||||
return text;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user