aboutsummaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-11-18 23:32:08 +0100
committerdec05eba <dec05eba@protonmail.com>2022-11-18 23:32:12 +0100
commitde45f6d8d7d777244006a7998ec971157e51296e (patch)
tree93ab54b196bda4af7d7fe16d74fb25f08c37a931 /src/plugins
parentc56cb5d25388d938fce485ff02b067a2fd70e096 (diff)
Readd meme gpg encryption in matrix, this time asynchronous decryption
of only visible items
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/Matrix.cpp218
1 files changed, 205 insertions, 13 deletions
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index 973571e..953543c 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -41,6 +41,145 @@ namespace QuickMedia {
static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\",\"m.room.canonical_alias\",\"m.space.child\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}";
static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\",\"m.room.canonical_alias\",\"m.space.child\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}";
+ static bool is_gpg_installed = false;
+ static bool gpg_installed_checked = false;
+
+ static std::string matrix_decrypt_gpg_message_if_needed(std::string message, bool &success) {
+ success = false;
+
+ if(!gpg_installed_checked) {
+ gpg_installed_checked = true;
+ is_gpg_installed = is_program_executable_by_name("gpg");
+ }
+
+ std::string result;
+ if(!is_gpg_installed || get_config().matrix.gpg_user_id.empty()) {
+ result = std::move(message);
+ } else {
+ size_t pgp_begin_index;
+ size_t pgp_end_index;
+ if((pgp_begin_index = message.find("-----BEGIN PGP MESSAGE-----")) != std::string::npos && (pgp_end_index = message.find("-----END PGP MESSAGE-----", pgp_begin_index + 27)) != std::string::npos) {
+ std::string decrypted_string;
+ const char *args[] = { "gpg", "-d", nullptr };
+ const std::string_view pgp_message(message.data() + pgp_begin_index, (pgp_end_index + 25) - pgp_begin_index);
+ if(exec_program_write_stdin(args, pgp_message.data(), pgp_message.size(), accumulate_string, &decrypted_string) != 0) {
+ result = "🔒 Failed to decrypt message:\n" + std::move(message);
+ } else {
+ decrypted_string.insert(0, "🔒 ", strlen("🔒 "));
+ result = std::move(message);
+ result.replace(result.begin() + pgp_begin_index, result.begin() + pgp_begin_index + pgp_message.size(), std::move(decrypted_string));
+ success = true;
+ }
+ } else {
+ result = std::move(message);
+ }
+ }
+ return result;
+ }
+
+ enum class GpgLineType {
+ UID,
+ FPR,
+ UNKNOWN
+ };
+
+ static std::string user_id_to_email_format(const std::string_view user_id) {
+ if(user_id.empty() || user_id[0] != '@')
+ return "";
+
+ size_t colon_index = user_id.find(':');
+ if(colon_index == std::string::npos)
+ return "";
+
+ return std::string(user_id.substr(1, colon_index - 1)) + "@" + std::string(user_id.substr(colon_index + 1));
+ }
+
+ static std::string_view gpg_display_name_extract_email(const std::string_view display_name) {
+ if(display_name.empty() || display_name.back() != '>')
+ return std::string_view();
+
+ size_t email_start_index = display_name.rfind('<');
+ if(email_start_index == std::string::npos)
+ return std::string_view();
+
+ email_start_index += 1;
+ return display_name.substr(email_start_index, display_name.size() - 1 - email_start_index);
+ }
+
+ static bool for_each_user_with_matching_gpg_key(const std::vector<std::shared_ptr<UserInfo>> &users, std::function<void(std::string_view pub_key)> callback) {
+ std::string output;
+ const char *args[] = { "gpg", "--list-keys", "--with-colons", nullptr };
+ if(exec_program(args, accumulate_string, &output) != 0)
+ return false;
+
+ std::unordered_set<std::string> users_by_email;
+ for(auto &user : users) {
+ std::string user_email = user_id_to_email_format(user->user_id);
+ if(!user_email.empty())
+ users_by_email.insert(std::move(user_email));
+ }
+
+ std::string_view fpr;
+ string_split(output, '\n', [&](const char *str, size_t size) {
+ GpgLineType line_type = GpgLineType::UNKNOWN;
+ int column = 0;
+
+ string_split_view(std::string_view(str, size), ':', [&](const char *str, size_t size) {
+ std::string_view section(str, size);
+ if(column == 0) {
+ if(section == "uid")
+ line_type = GpgLineType::UID;
+ else if(section == "fpr")
+ line_type = GpgLineType::FPR;
+ } else if(column == 9) {
+ if(line_type == GpgLineType::UID) {
+ // Assumes that each uid is preceeded with fpr
+ const std::string user_email = std::string(gpg_display_name_extract_email(section));
+ if(!user_email.empty() && users_by_email.find(user_email) != users_by_email.end()) {
+ fprintf(stderr, "found public key %.*s for user %s\n", (int)fpr.size(), fpr.data(), user_email.c_str());
+ callback(fpr);
+ }
+ } else if(line_type == GpgLineType::FPR) {
+ fpr = section;
+ }
+ }
+
+ ++column;
+ return true;
+ });
+
+ return true;
+ });
+
+ return true;
+ }
+
+ bool matrix_gpg_encrypt_for_each_user_in_room(Matrix *matrix, RoomData *room, const std::string &my_gpg_user_id, const std::string &str, std::string &encrypted_str) {
+ auto me = matrix->get_me(room);
+ if(!me) {
+ fprintf(stderr, "Error: matrix->get_me failed\n");
+ return false;
+ }
+
+ std::vector<std::string> user_public_keys;
+ const bool found_users = for_each_user_with_matching_gpg_key(room->get_users_excluding_me(me->user_id), [&](std::string_view pub_key) {
+ user_public_keys.emplace_back(pub_key);
+ });
+
+ if(!found_users) {
+ fprintf(stderr, "Error: gpg --list-keys failed\n");
+ return false;
+ }
+
+ std::vector<const char*> args = { "gpg", "-e", "-r", my_gpg_user_id.c_str(), "-u", my_gpg_user_id.c_str(), "--armor", "--trust-model", "always", "--comment", "This message requires you to use QuickMedia to view it and the user that sent it needs to have your key imported in gpg" };
+ for(const std::string &pub_key : user_public_keys) {
+ args.push_back("-r");
+ args.push_back(pub_key.c_str());
+ }
+ args.push_back(nullptr);
+ return exec_program_write_stdin(args.data(), str.c_str(), str.size(), accumulate_string, &encrypted_str) == 0;
+ }
+
static std::string capitalize(const std::string &str) {
if(str.size() >= 1)
return to_upper(str[0]) + str.substr(1);
@@ -163,6 +302,35 @@ namespace QuickMedia {
}
}
+ MatrixChatBodyItemData::~MatrixChatBodyItemData() {
+ if(decrypt_job)
+ decrypt_job->cancel = true;
+ }
+
+ void MatrixChatBodyItemData::draw_overlay(mgl::Window&, const Widgets &widgets) {
+ switch(decrypt_state) {
+ case DecryptState::NOT_DECRYPTED: {
+ decrypt_state = DecryptState::DECRYPTING;
+ decrypt_job = std::make_shared<MatrixChatBodyDecryptJob>();
+ decrypt_job->text = text_to_decrypt;
+ decrypt_job->decrypt_state = MatrixChatBodyDecryptJob::DecryptState::DECRYPTING;
+ matrix->async_decrypt_message(decrypt_job);
+ break;
+ }
+ case DecryptState::DECRYPTING: {
+ if(decrypt_job->decrypt_state == MatrixChatBodyDecryptJob::DecryptState::DECRYPTED || decrypt_job->decrypt_state == MatrixChatBodyDecryptJob::DecryptState::FAILED_TO_DECRYPT) {
+ decrypt_state = DecryptState::DECRYPTED;
+ widgets.body_item->set_description(std::move(decrypt_job->text));
+ decrypt_job.reset();
+ }
+ break;
+ }
+ case DecryptState::DECRYPTED: {
+ break;
+ }
+ }
+ }
+
bool TimestampedDisplayData::set_data_if_newer(std::string new_data, time_t new_timestamp) {
if(new_timestamp == 0) {
data = std::move(new_data);
@@ -307,6 +475,7 @@ namespace QuickMedia {
std::vector<std::shared_ptr<UserInfo>> RoomData::get_users_excluding_me(const std::string &my_user_id) {
std::lock_guard<std::mutex> lock(user_mutex);
std::vector<std::shared_ptr<UserInfo>> users_excluding_me;
+ // TODO: Optimize
for(auto &[user_id, user] : user_info_by_user_id) {
if(user->user_id != my_user_id) {
users_excluding_me.push_back(user);
@@ -687,6 +856,10 @@ namespace QuickMedia {
return body;
}
+ static bool should_message_by_decrypted(const std::string &text) {
+ return !get_config().matrix.gpg_user_id.empty() && text.find("-----BEGIN PGP MESSAGE-----") != std::string::npos && text.find("-----END PGP MESSAGE-----") != std::string::npos;
+ }
+
void MatrixQuickMedia::update_room_description(RoomData *room, const Messages &new_messages, bool is_initial_sync, bool sync_is_cache) {
time_t read_marker_message_timestamp = 0;
std::shared_ptr<UserInfo> me = matrix->get_me(room);
@@ -756,11 +929,13 @@ namespace QuickMedia {
if(!unread_mentions && set_room_as_unread)
room_desc += "Unread: ";
- room->latest_message = extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message, custom_emoji_max_size);
- room_desc += room->latest_message;
+ room->offset_to_latest_message_text = room_desc.size();
+ room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message, custom_emoji_max_size);
room->body_item->set_description(std::move(room_desc));
room->body_item->set_description_max_lines(3);
+ if(should_message_by_decrypted(room->body_item->get_description()))
+ room->body_item->extra = std::make_shared<MatrixChatBodyItemData>(matrix, room->body_item->get_description());
if(set_room_as_unread)
room->body_item->set_title_color(get_theme().attention_alert_text_color, true);
room->last_message_read = false;
@@ -768,10 +943,12 @@ namespace QuickMedia {
rooms_page->move_room_to_top(room);
room_tags_page->move_room_to_top(room);
} else if(last_new_message) {
+ room->offset_to_latest_message_text = 0;
room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_new_message.get(), custom_emoji_max_size));
room->body_item->set_description_color(get_theme().faded_text_color);
room->body_item->set_description_max_lines(3);
- room->latest_message = room->body_item->get_description();
+ if(should_message_by_decrypted(room->body_item->get_description()))
+ room->body_item->extra = std::make_shared<MatrixChatBodyItemData>(matrix, room->body_item->get_description());
rooms_page->move_room_to_top(room);
room_tags_page->move_room_to_top(room);
@@ -1763,6 +1940,10 @@ namespace QuickMedia {
custom_emoji_by_key.clear();
silenced_invites.clear();
qm_read_markers_by_room_cache.clear();
+ if(decrypt_thread.joinable()) {
+ decrypt_task.close();
+ decrypt_thread.join();
+ }
}
bool Matrix::is_initial_sync_finished() {
@@ -2674,14 +2855,6 @@ namespace QuickMedia {
mgl::vec2i image_max_size;
};
- static int accumulate_string(char *data, int size, void *userdata) {
- std::string *str = (std::string*)userdata;
- if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable
- return 1;
- str->append(data, size);
- return 0;
- }
-
// TODO: Full proper parsing with tag depth
static int formattext_text_parser_callback(HtmlParser *html_parser, HtmlParseType parse_type, void *userdata) {
FormattedTextParseUserdata &parse_userdata = *(FormattedTextParseUserdata*)userdata;
@@ -3882,7 +4055,7 @@ namespace QuickMedia {
if(is_inside_code_block)
formatted_body += '\n';
else
- formatted_body += "<br/>";
+ formatted_body += "<br>";
}
std::string line_str(str, size);
@@ -3924,6 +4097,26 @@ namespace QuickMedia {
}
+ void Matrix::async_decrypt_message(std::shared_ptr<MatrixChatBodyDecryptJob> decrypt_job) {
+ if(!decrypt_thread.joinable()) {
+ decrypt_thread = std::thread([this]() {
+ while(decrypt_task.is_running()) {
+ auto decrypt_job = decrypt_task.pop_wait();
+ if(!decrypt_job)
+ return;
+
+ if(decrypt_job.value()->cancel)
+ continue;
+
+ bool success = false;
+ decrypt_job.value()->text = matrix_decrypt_gpg_message_if_needed(std::move(decrypt_job.value()->text), success);
+ decrypt_job.value()->decrypt_state = success ? MatrixChatBodyDecryptJob::DecryptState::DECRYPTED : MatrixChatBodyDecryptJob::DecryptState::FAILED_TO_DECRYPT;
+ }
+ });
+ }
+ decrypt_task.push(std::move(decrypt_job));
+ }
+
PluginResult Matrix::post_message(RoomData *room, const std::string &body, std::string &event_id_response, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype, const std::string &custom_transaction_id) {
std::string transaction_id = custom_transaction_id;
if(transaction_id.empty())
@@ -5855,7 +6048,6 @@ namespace QuickMedia {
}
void Matrix::update() {
- mgl::Clock timer;
std::optional<std::function<void()>> task;
while((task = ui_thread_tasks.pop_if_available()) != std::nullopt) {
task.value()();