From 1ce0bca5a88b2a4e7d79892da907410f2ad0775d Mon Sep 17 00:00:00 2001 From: saturneric Date: Tue, 26 Nov 2024 20:58:17 +0100 Subject: [PATCH] feat: support eml data decryption --- src/m_email/EMailModule.cpp | 197 +++++++++++++++++++++++++++++++++--- 1 file changed, 182 insertions(+), 15 deletions(-) diff --git a/src/m_email/EMailModule.cpp b/src/m_email/EMailModule.cpp index f2747c7..582542a 100644 --- a/src/m_email/EMailModule.cpp +++ b/src/m_email/EMailModule.cpp @@ -61,6 +61,7 @@ auto GFRegisterModule() -> int { MLogDebug("email module registering..."); LISTEN("EMAIL_VERIFY_EML_DATA"); + LISTEN("EMAIL_DECRYPT_EML_DATA"); return 0; } @@ -78,7 +79,7 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { message->parse(vmime_data); } catch (const vmime::exception& e) { FLOG_DEBUG("error when parsing vmime data: %1", e.what()); - CB_ERR(event, -2, "error when parsing vmime data"); + CB_ERR(event, -2, "Error when parsing eml raw data"); } auto header = message->getHeader(); @@ -86,7 +87,7 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { auto content_type_field = header->getField(vmime::fields::CONTENT_TYPE); if (!content_type_field) { - CB_ERR(event, -2, "cannot get 'Content-Type' Field from header"); + CB_ERR(event, -2, "Cannot get 'Content-Type' Field from header"); } auto content_type_value = @@ -94,7 +95,7 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { auto prm_protocol = content_type_field->getParameter("protocol"); if (!prm_protocol) { - CB_ERR(event, -2, "cannot get 'protocol' from 'Content-Type'"); + CB_ERR(event, -2, "Cannot get 'protocol' from 'Content-Type'"); } /* @@ -113,7 +114,7 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { auto prm_protocol_value = Q_SC(prm_protocol->getValue().generate()); if (prm_protocol_value != "application/pgp-signature") CB_ERR(event, -2, - "'protocol' parameter which MUST have a value of " + "The 'protocol' parameter which MUST have a value of " "'application/pgp-signature' (MUST be quoted)"); auto prm_micalg = content_type_field->getParameter("micalg"); @@ -131,8 +132,8 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { FLOG_DEBUG("micalg value: %1", prm_micalg_value); if (!IsValidMicalgFormat(prm_micalg_value)) { CB_ERR(event, -2, - "'micalg' MUST contain exactly one hash-symbol of the format " - "'pgp-'"); + "The 'micalg' parameter MUST contain exactly one hash-symbol of the " + "format 'pgp-'"); } auto from_field_value_text = @@ -176,21 +177,39 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { auto part_mime_content_text = QByteArray::fromStdString( vmime_data.substr(part_mime_parse_offset, part_mime_parse_length)); - FLOG_DEBUG("body part of raw offset: %1, length: %2", part_mime_parse_offset, - part_mime_parse_length); - FLOG_DEBUG("body part of raw content left: %1", - part_mime_content_text.left(64)); - FLOG_DEBUG("body part of raw content right: %1", - part_mime_content_text.right(64)); + FLOG_DEBUG("mime part info, raw offset: %1, length: %2", + part_mime_parse_offset, part_mime_parse_length); auto part_mime_content_hash = QCryptographicHash::hash( part_mime_content_text, QCryptographicHash::Sha1); - FLOG_DEBUG("body part of raw content hash: %1", + FLOG_DEBUG("mime part of raw content hash: %1", part_mime_content_hash.toHex()); if (part_mime_content_text.isEmpty()) CB_ERR(event, -2, "mime raw data part is empty"); + auto attachments = + vmime::attachmentHelper::findAttachmentsInBodyPart(part_mime); + FLOG_DEBUG("mime part info, attachment count: %1", attachments.size()); + + QStringList public_keys_buffer; + + for (const auto& att : attachments) { + auto att_type = Q_SC(att->getType().generate()).trimmed(); + FLOG_DEBUG("mime part info, attachment type: %1", att_type); + + if (att_type != "application/pgp-keys") continue; + + std::ostringstream oss; + vmime::utility::outputStreamAdapter osa(oss); + att->getData()->extract(osa); + + public_keys_buffer.append(Q_SC(oss.str())); + } + + FLOG_DEBUG("mime part info, attached public keys: ", + public_keys_buffer.join("\n")); + /* * The second body MUST contain the OpenPGP digital signature. It MUST * be labeled with a content type of "application/pgp-signature" @@ -209,7 +228,7 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { auto part_sign_body_content = QByteArray::fromStdString(part_sign->getBody()->generate()); if (part_sign_body_content.trimmed().isEmpty()) - CB_ERR(event, -2, "signature part is empty"); + CB_ERR(event, -2, "The signature part is empty"); FLOG_DEBUG("body part of signature content: %1", part_sign_body_content); @@ -218,7 +237,7 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { { {"ret", QString::number(0)}, {"mime", QString::fromLatin1(part_mime_content_text.toBase64())}, - {"mime_hash", part_mime_content_hash}, + {"mime_hash", part_mime_content_hash.toHex()}, {"signature", QString::fromLatin1(part_sign_body_content.toBase64())}, {"from", from_field_value_text}, {"to", to_field_value_text}, @@ -227,6 +246,154 @@ REGISTER_EVENT_HANDLER(EMAIL_VERIFY_EML_DATA, [](const MEvent& event) -> int { {"subject", subject_field_value_text}, {"datetime", QString::number(date_field_value.toMSecsSinceEpoch())}, {"micalg", prm_micalg_value}, + {"public_keys", public_keys_buffer.join("\n")}, + }); + + return 0; +}); + +REGISTER_EVENT_HANDLER(EMAIL_DECRYPT_EML_DATA, [](const MEvent& event) -> int { + if (event["eml_data"].isEmpty()) CB_ERR(event, -1, "eml_data is empty"); + + auto data = QByteArray::fromBase64(QString(event["eml_data"]).toLatin1()); + vmime::string vmime_data(data.constData(), data.size()); + + auto message = vmime::make_shared(); + try { + message->parse(vmime_data); + } catch (const vmime::exception& e) { + FLOG_DEBUG("error when parsing vmime data: %1", e.what()); + CB_ERR(event, -2, "error when parsing vmime data"); + } + + auto header = message->getHeader(); + + auto content_type_field = + header->getField(vmime::fields::CONTENT_TYPE); + if (!content_type_field) { + CB_ERR(event, -2, "cannot get 'Content-Type' Field from header"); + } + + auto content_type_value = + Q_SC(content_type_field->getValue()->generate()).trimmed(); + + auto prm_protocol = content_type_field->getParameter("protocol"); + if (!prm_protocol) { + CB_ERR(event, -2, "cannot get 'protocol' from 'Content-Type'"); + } + + /* + * OpenPGP encrypted data is denoted by the "multipart/encrypted" + * content type + */ + if (content_type_value != "multipart/encrypted") + CB_ERR(event, -2, + "OpenPGP encrypted data is denoted by the 'multipart/encrypted' " + "content type"); + + /* + * MUST have a "protocol" parameter value of "application/pgp-encrypted" + */ + auto prm_protocol_value = Q_SC(prm_protocol->getValue().generate()); + if (prm_protocol_value != "application/pgp-encrypted") + CB_ERR(event, -2, + "'protocol' parameter which MUST have a value of " + "'application/pgp-encrypted' (MUST be quoted)"); + + auto from_field_value_text = + ExtractFieldValueMailBox(header, vmime::fields::FROM); + auto to_field_value_text = + ExtractFieldValueAddressList(header, vmime::fields::TO); + auto cc_field_value_text = + ExtractFieldValueAddressList(header, vmime::fields::CC); + auto bcc_field_value_text = + ExtractFieldValueAddressList(header, vmime::fields::BCC); + auto date_field_value = + ExtractFieldValueDateTime(header, vmime::fields::DATE); + auto subject_field_value_text = + ExtractFieldValueText(header, vmime::fields::SUBJECT); + auto reply_to_field_value_text = + ExtractFieldValueMailBox(header, vmime::fields::REPLY_TO); + auto organization_text = + ExtractFieldValueText(header, vmime::fields::ORGANIZATION); + + auto body = message->getBody(); + auto content_type = body->getContentType(); + auto part_count = body->getPartCount(); + + FLOG_DEBUG("body page count: %1", part_count); + + /* + * The multipart/encrypted body MUST consist of exactly two parts. + */ + if (part_count != 2) + CB_ERR(event, -2, + "The multipart/signed body MUST consist of exactly two parts"); + + /* + * The multipart/encrypted MIME body MUST consist of exactly two body + * parts, the first with content type "application/pgp-encrypted". This + * body contains the control information. + */ + auto part_mime = body->getPartAt(0); + + std::ostringstream oss; + vmime::utility::outputStreamAdapter osa(oss); + + auto part_mime_body = part_mime->getBody(); + auto part_mime_body_content = part_mime_body->getContents(); + if (!part_mime_body_content) { + CB_ERR(event, -2, "Cannot get the content of the first part's body"); + } + + part_mime_body_content->extractRaw(osa); + osa.flush(); + + auto part_mime_body_content_text = Q_SC(oss.str()); + FLOG_DEBUG("body part of raw content text: %1", part_mime_body_content_text); + + /* + * A message complying with this + * standard MUST contain a "Version: 1" field in this body. + */ + if (!part_mime_body_content_text.contains("Version: 1")) { + CB_ERR(event, -2, + "The first part MUST contain a 'Version: 1' field in this body."); + } + + /* + * The second MIME body part MUST contain the actual encrypted data. It + * MUST be labeled with a content type of "application/octet-stream". + */ + auto part_sign = body->getPartAt(1); + auto part_sign_header = part_sign->getHeader(); + auto part_sign_content_type = part_sign_header->ContentType(); + auto part_sign_content_type_value = + Q_SC(part_sign_content_type->getValue()->generate()); + + if (part_sign_content_type_value != "application/octet-stream") + CB_ERR(event, -2, + "The second part MUST be labeled with a content type of " + "'application/octet-stream'"); + + auto part_encr_body_content = + QByteArray::fromStdString(part_sign->getBody()->generate()); + if (part_encr_body_content.trimmed().isEmpty()) + CB_ERR(event, -2, "The second part is empty"); + + FLOG_DEBUG("body part of encrypt content: %1", part_encr_body_content); + + // callback + CB(event, GFGetModuleID(), + { + {"ret", QString::number(0)}, + {"encrypted", QString::fromLatin1(part_encr_body_content.toBase64())}, + {"from", from_field_value_text}, + {"to", to_field_value_text}, + {"cc", cc_field_value_text}, + {"bcc", bcc_field_value_text}, + {"subject", subject_field_value_text}, + {"datetime", QString::number(date_field_value.toMSecsSinceEpoch())}, }); return 0;