diff --git a/autotests/core/cryptohelpertest.cpp b/autotests/core/cryptohelpertest.cpp index ed807d7..e7f3846 100644 --- a/autotests/core/cryptohelpertest.cpp +++ b/autotests/core/cryptohelpertest.cpp @@ -1,186 +1,184 @@ // SPDX-FileCopyrightText: 2015 Sandro Knauß // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include "cryptohelpertest.h" #include "cryptohelper.h" #include using namespace MimeTreeParser; QByteArray readMailFromFile(const QString &mailFile) { QFile file(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile); file.open(QIODevice::ReadOnly); Q_ASSERT(file.isOpen()); return file.readAll(); } void CryptoHelperTest::testPMFDEmpty() { QCOMPARE(prepareMessageForDecryption("").count(), 0); } void CryptoHelperTest::testPMFDWithNoPGPBlock() { const QByteArray text = "testblabla"; const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 1); QCOMPARE(blocks[0].text(), text); QCOMPARE(blocks[0].type(), NoPgpBlock); } void CryptoHelperTest::testPGPBlockType() { const QString blockText = QStringLiteral("text"); const QString preString = QStringLiteral("before\n"); for (int i = 1; i <= PrivateKeyBlock; ++i) { QString name; switch (i) { case PgpMessageBlock: name = QStringLiteral("MESSAGE"); break; case MultiPgpMessageBlock: name = QStringLiteral("MESSAGE PART"); break; case SignatureBlock: name = QStringLiteral("SIGNATURE"); break; case ClearsignedBlock: name = QStringLiteral("SIGNED MESSAGE"); break; case PublicKeyBlock: name = QStringLiteral("PUBLIC KEY BLOCK"); break; case PrivateKeyBlock: name = QStringLiteral("PRIVATE KEY BLOCK"); break; } QString text = QLatin1StringView("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText; QList blocks = prepareMessageForDecryption(preString.toLatin1() + text.toLatin1()); QCOMPARE(blocks.count(), 1); QCOMPARE(blocks[0].type(), UnknownBlock); text += QLatin1StringView("\n-----END PGP ") + name + QLatin1Char('\n'); blocks = prepareMessageForDecryption(preString.toLatin1() + text.toLatin1()); QCOMPARE(blocks.count(), 2); QCOMPARE(blocks[1].text(), text.toLatin1()); QCOMPARE(blocks[1].type(), static_cast(i)); } } void CryptoHelperTest::testDeterminePGPBlockType() { const QString blockText = QStringLiteral("text"); for (int i = 1; i <= PrivateKeyBlock; ++i) { QString name; switch (i) { case PgpMessageBlock: name = QStringLiteral("MESSAGE"); break; case MultiPgpMessageBlock: name = QStringLiteral("MESSAGE PART"); break; case SignatureBlock: name = QStringLiteral("SIGNATURE"); break; case ClearsignedBlock: name = QStringLiteral("SIGNED MESSAGE"); break; case PublicKeyBlock: name = QStringLiteral("PUBLIC KEY BLOCK"); break; case PrivateKeyBlock: name = QStringLiteral("PRIVATE KEY BLOCK"); break; } const QString text = QLatin1StringView("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText + QLatin1Char('\n'); const Block block = Block(text.toLatin1()); QCOMPARE(block.text(), text.toLatin1()); QCOMPARE(block.type(), static_cast(i)); } } void CryptoHelperTest::testEmbededPGPBlock() { const QByteArray text = QByteArray("before\n-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\nafter"); const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 3); QCOMPARE(blocks[0].text(), QByteArray("before\n")); QCOMPARE(blocks[1].text(), QByteArray("-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n")); QCOMPARE(blocks[2].text(), QByteArray("after")); } void CryptoHelperTest::testClearSignedMessage() { const QByteArray text = QByteArray( "before\n-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\nafter"); const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 3); QCOMPARE(blocks[0].text(), QByteArray("before\n")); QCOMPARE(blocks[1].text(), QByteArray("-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\n")); QCOMPARE(blocks[2].text(), QByteArray("after")); } void CryptoHelperTest::testMultipleBlockMessage() { const QByteArray text = QByteArray( "before\n-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP " "SIGNATURE-----\nafter\n-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n"); const QList blocks = prepareMessageForDecryption(text); QCOMPARE(blocks.count(), 4); QCOMPARE(blocks[0].text(), QByteArray("before\n")); QCOMPARE(blocks[1].text(), QByteArray("-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\n")); QCOMPARE(blocks[2].text(), QByteArray("after\n")); QCOMPARE(blocks[3].text(), QByteArray("-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n")); } void CryptoHelperTest::testDecryptMessage() { auto message = KMime::Message::Ptr(new KMime::Message); message->setContent(readMailFromFile(QLatin1StringView("openpgp-encrypted+signed.mbox"))); message->parse(); bool wasEncrypted = false; GpgME::Protocol protocol; auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol); QVERIFY(wasEncrypted); QVERIFY(decryptedMessage); QCOMPARE(decryptedMessage->decodedContent(), QByteArray("encrypted message text")); QCOMPARE(decryptedMessage->encodedContent(), QByteArray("From test@kolab.org Wed, 08 Sep 2010 17: 02:52 +0200\nFrom: OpenPGP Test \nTo: test@kolab.org\nSubject: OpenPGP " "encrypted\nDate: Wed, 08 Sep 2010 17:02:52 +0200\nUser-Agent: KMail/4.6 pre (Linux/2.6.34-rc2-2-default; KDE/4.5.60; x86_64; ; " ")\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7Bit\nContent-Type: text/plain; charset=\"us-ascii\"\n\nencrypted message text")); QCOMPARE(protocol, GpgME::OpenPGP); } void CryptoHelperTest::testDecryptInlineMessage() { auto message = KMime::Message::Ptr(new KMime::Message); message->setContent(readMailFromFile(QLatin1StringView("openpgp-inline-encrypted+nonenc.mbox"))); message->parse(); - qWarning() << message->decodedContent(); - bool wasEncrypted = false; GpgME::Protocol protocol; auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol); QVERIFY(wasEncrypted); QVERIFY(decryptedMessage); QCOMPARE(decryptedMessage->decodedContent(), QByteArray("Not encrypted not signed :(\n\nsome random text\n")); qWarning() << decryptedMessage->encodedContent(); QCOMPARE(decryptedMessage->encodedContent(), QByteArray("From test@kolab.org Wed, 25 May 2011 23: 49:40 +0100\nFrom: OpenPGP Test \nTo: test@kolab.org\nSubject: " "inlinepgpencrypted + non enc text\nDate: Wed, 25 May 2011 23:49:40 +0100\nMessage-ID: " "<1786696.yKXrOjjflF@herrwackelpudding.localhost>\nX-KMail-Transport: GMX\nX-KMail-Fcc: 28\nX-KMail-Drafts: 7\nX-KMail-Templates: " "9\nUser-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64;\n git-0269848; 2011-04-19)\nMIME-Version: " "1.0\nContent-Type: text/plain; charset=\"us-ascii\"\n\nNot encrypted not signed :(\n\nsome random text\n")); QCOMPARE(protocol, GpgME::OpenPGP); } QTEST_APPLESS_MAIN(CryptoHelperTest) #include "moc_cryptohelpertest.cpp" diff --git a/src/core/cryptohelper.cpp b/src/core/cryptohelper.cpp index 9f2088f..f0dba47 100644 --- a/src/core/cryptohelper.cpp +++ b/src/core/cryptohelper.cpp @@ -1,317 +1,318 @@ // SPDX-FileCopyrightText: 2001,2002 the KPGP authors // SPDX-FileCopyrightText: 2015 Sandro Knauß // SPDX-FileCopyrightText: 2017 Daniel Vrátil // SPDX-License-Identifier: GPL-2.0-or-later #include "cryptohelper.h" #include "mimetreeparser_core_debug.h" #include #include #include #include #include #include using namespace MimeTreeParser; PGPBlockType Block::determineType() const { const QByteArray data = text(); if (data.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { return NoPgpBlock; } else if (data.startsWith("-----BEGIN PGP SIGNED")) { return ClearsignedBlock; } else if (data.startsWith("-----BEGIN PGP SIGNATURE")) { return SignatureBlock; } else if (data.startsWith("-----BEGIN PGP PUBLIC")) { return PublicKeyBlock; } else if (data.startsWith("-----BEGIN PGP PRIVATE") || data.startsWith("-----BEGIN PGP SECRET")) { return PrivateKeyBlock; } else if (data.startsWith("-----BEGIN PGP MESSAGE")) { if (data.startsWith("-----BEGIN PGP MESSAGE PART")) { return MultiPgpMessageBlock; } else { return PgpMessageBlock; } } else if (data.startsWith("-----BEGIN PGP ARMORED FILE")) { return PgpMessageBlock; } else if (data.startsWith("-----BEGIN PGP ")) { return UnknownBlock; } else { return NoPgpBlock; } } QList MimeTreeParser::prepareMessageForDecryption(const QByteArray &msg) { PGPBlockType pgpBlock = NoPgpBlock; QList blocks; int start = -1; // start of the current PGP block int lastEnd = -1; // end of the last PGP block const int length = msg.length(); if (msg.isEmpty()) { return blocks; } if (msg.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) { return blocks; } if (msg.startsWith("-----BEGIN PGP ")) { start = 0; } else { start = msg.indexOf("\n-----BEGIN PGP ") + 1; if (start == 0) { blocks.append(Block(msg, NoPgpBlock)); return blocks; } } while (start != -1) { int nextEnd; int nextStart; // is the PGP block a clearsigned block? if (!strncmp(msg.constData() + start + 15, "SIGNED", 6)) { pgpBlock = ClearsignedBlock; } else { pgpBlock = UnknownBlock; } nextEnd = msg.indexOf("\n-----END PGP ", start + 15); nextStart = msg.indexOf("\n-----BEGIN PGP ", start + 15); if (nextEnd == -1) { // Missing END PGP line if (lastEnd != -1) { blocks.append(Block(msg.mid(lastEnd + 1), UnknownBlock)); } else { blocks.append(Block(msg.mid(start), UnknownBlock)); } break; } if ((nextStart == -1) || (nextEnd < nextStart) || (pgpBlock == ClearsignedBlock)) { // most likely we found a PGP block (but we don't check if it's valid) // store the preceding non-PGP block if (start - lastEnd - 1 > 0) { blocks.append(Block(msg.mid(lastEnd + 1, start - lastEnd - 1), NoPgpBlock)); } lastEnd = msg.indexOf("\n", nextEnd + 14); if (lastEnd == -1) { if (start < length) { blocks.append(Block(msg.mid(start))); } break; } else { blocks.append(Block(msg.mid(start, lastEnd + 1 - start))); if ((nextStart != -1) && (nextEnd > nextStart)) { nextStart = msg.indexOf("\n-----BEGIN PGP ", lastEnd + 1); } } } start = nextStart; if (start == -1) { if (lastEnd + 1 < length) { // rest of mail is no PGP Block blocks.append(Block(msg.mid(lastEnd + 1), NoPgpBlock)); } break; } else { start++; // move start behind the '\n' } } return blocks; } Block::Block() = default; Block::Block(const QByteArray &m) : msg(m) { mType = determineType(); } Block::Block(const QByteArray &m, PGPBlockType t) : msg(m) , mType(t) { } QByteArray MimeTreeParser::Block::text() const { return msg; } PGPBlockType Block::type() const { return mType; } namespace { bool isPGP(const KMime::Content *part, bool allowOctetStream = false) { const auto ct = static_cast(part->headerByType("Content-Type")); return ct && (ct->isSubtype("pgp-encrypted") || ct->isSubtype("encrypted") || (allowOctetStream && ct->isMimeType("application/octet-stream"))); } bool isSMIME(const KMime::Content *part) { const auto ct = static_cast(part->headerByType("Content-Type")); return ct && (ct->isSubtype("pkcs7-mime") || ct->isSubtype("x-pkcs7-mime")); } void copyHeader(const KMime::Headers::Base *header, KMime::Message::Ptr msg) { auto newHdr = KMime::Headers::createHeader(header->type()); if (!newHdr) { newHdr = new KMime::Headers::Generic(header->type()); } newHdr->from7BitString(header->as7BitString(false)); msg->appendHeader(newHdr); } bool isContentHeader(const KMime::Headers::Base *header) { return header->is("Content-Type") || header->is("Content-Transfer-Encoding") || header->is("Content-Disposition"); } KMime::Message::Ptr assembleMessage(const KMime::Message::Ptr &orig, const KMime::Content *newContent) { auto out = KMime::Message::Ptr::create(); // Use the new content as message content out->setBody(const_cast(newContent)->encodedBody()); out->parse(); // remove default explicit content headers added by KMime::Content::parse() QList headers = out->headers(); for (const auto hdr : std::as_const(headers)) { if (isContentHeader(hdr)) { out->removeHeader(hdr->type()); } } // Copy over headers from the original message, except for CT, CTE and CD // headers, we want to preserve those from the new content headers = orig->headers(); for (const auto hdr : std::as_const(headers)) { if (isContentHeader(hdr)) { continue; } copyHeader(hdr, out); } // Overwrite some headers by those provided by the new content headers = newContent->headers(); for (const auto hdr : std::as_const(headers)) { if (isContentHeader(hdr)) { copyHeader(hdr, out); } } out->assemble(); out->parse(); return out; } } KMime::Message::Ptr CryptoUtils::decryptMessage(const KMime::Message::Ptr &msg, bool &wasEncrypted, GpgME::Protocol &protoName) { protoName = GpgME::UnknownProtocol; bool multipart = false; if (msg->contentType(false) && msg->contentType(false)->isMimeType("multipart/encrypted")) { multipart = true; const auto subparts = msg->contents(); for (auto subpart : subparts) { if (isPGP(subpart, true)) { protoName = GpgME::OpenPGP; break; } else if (isSMIME(subpart)) { protoName = GpgME::CMS; break; } } } else { if (isPGP(msg.data())) { protoName = GpgME::OpenPGP; } else if (isSMIME(msg.data())) { protoName = GpgME::CMS; } else { const auto blocks = prepareMessageForDecryption(msg->body()); QByteArray content; for (const auto &block : blocks) { if (block.type() == PgpMessageBlock) { const auto proto = QGpgME::openpgp(); + protoName = GpgME::OpenPGP; wasEncrypted = true; QByteArray outData; auto inData = block.text(); auto decrypt = proto->decryptJob(); auto ctx = QGpgME::Job::context(decrypt); ctx->setDecryptionFlags(GpgME::Context::DecryptUnwrap); auto result = decrypt->exec(inData, outData); if (result.error()) { // unknown key, invalid algo, or general error qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to decrypt:" << result.error().asString(); return {}; } inData = outData; auto verify = proto->verifyOpaqueJob(true); auto resultVerify = verify->exec(inData, outData); if (resultVerify.error()) { qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to verify:" << resultVerify.error().asString(); return {}; } content += KMime::CRLFtoLF(outData); } else if (block.type() == NoPgpBlock) { content += block.text(); } } KMime::Content decCt; decCt.setBody(content); decCt.parse(); decCt.assemble(); return assembleMessage(msg, &decCt); } } if (protoName == GpgME::UnknownProtocol) { // Not encrypted, or we don't recognize the encryption wasEncrypted = false; return {}; } const auto proto = (protoName == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); wasEncrypted = true; QByteArray outData; auto inData = multipart ? msg->encodedContent() : msg->decodedContent(); // decodedContent in fact returns decoded body auto decrypt = proto->decryptJob(); auto result = decrypt->exec(inData, outData); if (result.error()) { // unknown key, invalid algo, or general error qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to decrypt:" << result.error().asString(); return {}; } KMime::Content decCt; decCt.setContent(KMime::CRLFtoLF(outData)); decCt.parse(); decCt.assemble(); return assembleMessage(msg, &decCt); }