From de45f6d8d7d777244006a7998ec971157e51296e Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 18 Nov 2022 23:32:08 +0100 Subject: Readd meme gpg encryption in matrix, this time asynchronous decryption of only visible items --- src/plugins/Matrix.cpp | 218 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 205 insertions(+), 13 deletions(-) (limited to 'src/plugins') 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> &users, std::function 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 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 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 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(); + 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> RoomData::get_users_excluding_me(const std::string &my_user_id) { std::lock_guard lock(user_mutex); std::vector> 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 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(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(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 += "
"; + formatted_body += "
"; } std::string line_str(str, size); @@ -3924,6 +4097,26 @@ namespace QuickMedia { } + void Matrix::async_decrypt_message(std::shared_ptr 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 &file_info, const std::optional &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> task; while((task = ui_thread_tasks.pop_if_available()) != std::nullopt) { task.value()(); -- cgit v1.2.3