diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,9 +22,9 @@ set(MAILCOMMON_LIB_VERSION ${PIM_VERSION}) set(AKONADIMIME_LIB_VERSION "5.6.40") -set(MESSAGELIB_LIB_VERSION "5.6.40") +set(MESSAGELIB_LIB_VERSION "5.6.42") set(QT_REQUIRED_VERSION "5.7.0") -set(KMIME_LIB_VERSION "5.6.40") +set(KMIME_LIB_VERSION "5.6.41") set(KMAILTRANSPORT_LIB_VERSION "5.6.40") set(MAILIMPORTER_LIB_VERSION "5.6.40") set(LIBKDEPIM_LIB_VERSION "5.6.40") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,8 +19,10 @@ filter/filteractions/filteractionaddtag.cpp filter/filteractions/filteractionaddtoaddressbook.cpp filter/filteractions/filteractioncopy.cpp + filter/filteractions/filteractiondecrypt.cpp filter/filteractions/filteractiondelete.cpp filter/filteractions/filteractiondict.cpp + filter/filteractions/filteractionencrypt.cpp filter/filteractions/filteractionexec.cpp filter/filteractions/filteractionforward.cpp filter/filteractions/filteractionmove.cpp @@ -40,6 +42,7 @@ filter/filteractions/filteractionwidget.cpp filter/filteractions/filteractionwithaddress.cpp filter/filteractions/filteractionwithcommand.cpp + filter/filteractions/filteractionwithcrypto.cpp filter/filteractions/filteractionwithfolder.cpp filter/filteractions/filteractionwithnone.cpp filter/filteractions/filteractionwithstring.cpp diff --git a/src/filter/autotests/CMakeLists.txt b/src/filter/autotests/CMakeLists.txt --- a/src/filter/autotests/CMakeLists.txt +++ b/src/filter/autotests/CMakeLists.txt @@ -4,6 +4,8 @@ KF5::MailTransport KF5::I18n ) +add_definitions(-DTEST_PATH=\"${CMAKE_CURRENT_SOURCE_DIR}\") + macro(add_mailcommon_filter_test _name) ecm_add_test(${ARGN} TEST_NAME ${_name} @@ -49,6 +51,22 @@ ${filter_common_SRCS} ) +add_mailcommon_filter_test(filteractionencrypttest + filteractionencrypttest.cpp + gpghelper.cpp + ../filteractions/filteractionencrypt.cpp + ../filteractions/filteractionwithcrypto.cpp + ${filter_common_SRCS} +) + +add_mailcommon_filter_test(filteractiondecrypttest + filteractiondecrypttest.cpp + gpghelper.cpp + ../filteractions/filteractiondecrypt.cpp + ../filteractions/filteractionwithcrypto.cpp + ${filter_common_SRCS} +) + add_mailcommon_filter_test(filteractionrewriteheadertest filteractionrewriteheadertest.cpp ../filteractions/filteractionrewriteheader.cpp diff --git a/src/filter/autotests/filteractiondecrypttest.h b/src/filter/autotests/filteractiondecrypttest.h new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractiondecrypttest.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef FILTERACTIONDECRYPTTEST_H_ +#define FILTERACTIONDECRYPTTEST_H_ + +#include + +#include "gpghelper.h" + +class FilterActionDecryptTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void shouldDecrypt_data(); + void shouldDecrypt(); + +private: + GPGHelper *mGpg = {}; +}; + +#endif + diff --git a/src/filter/autotests/filteractiondecrypttest.cpp b/src/filter/autotests/filteractiondecrypttest.cpp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractiondecrypttest.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "filteractiondecrypttest.h" +#include "../filteractions/filteractiondecrypt.h" + +#include + +void FilterActionDecryptTest::initTestCase() +{ + mGpg = new GPGHelper(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpghome")); + QVERIFY(mGpg->isValid()); +} + +void FilterActionDecryptTest::cleanupTestCase() +{ + delete mGpg; +} + +void FilterActionDecryptTest::shouldDecrypt_data() +{ + QTest::addColumn("content"); + QTest::addColumn("encrypted"); + + QDir testDir(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpgdata")); + const auto tests = testDir.entryInfoList({ QStringLiteral("*.msg") }, QDir::Files, QDir::Name); + for (const auto test : tests) { + QFile plain(test.absoluteFilePath()); + QVERIFY(plain.open(QIODevice::ReadOnly)); + const auto plainData = plain.readAll(); + + QFile pgp(test.absoluteFilePath() + QStringLiteral(".pgp")); + QVERIFY(pgp.open(QIODevice::ReadOnly)); + QTest::newRow(QStringLiteral("PGP %1").arg(test.baseName()).toUtf8().constData()) + << plainData << pgp.readAll(); + + QFile smime(test.absoluteFilePath() + QStringLiteral(".smime")); + QVERIFY(smime.open(QIODevice::ReadOnly)); + QTest::newRow(QStringLiteral("SMIME %1").arg(test.baseName()).toUtf8().constData()) + << plainData << smime.readAll(); + + QTest::newRow(QStringLiteral("PLAIN %1").arg(test.baseName()).toUtf8().constData()) + << plainData << plainData; + } + +} + +void FilterActionDecryptTest::shouldDecrypt() +{ + QFETCH(QByteArray, content); + QFETCH(QByteArray, encrypted); + + MailCommon::FilterActionDecrypt action(this); + + auto msg = KMime::Message::Ptr::create(); + msg->setContent(encrypted); + msg->parse(); + msg->assemble(); + + Akonadi::Item item; + item.setPayload(msg); + + MailCommon::ItemContext context(item, true); + const auto result = action.process(context, false); + QCOMPARE(result, MailCommon::FilterAction::GoOn); + if (content != encrypted) { + QVERIFY(context.needsPayloadStore()); + } else { + // the message is not encrypted, no change is needed + QVERIFY(!context.needsPayloadStore()); + } + + auto newMsg = context.item().payload(); + QCOMPARE(newMsg->from()->asUnicodeString(), msg->from()->asUnicodeString()); + QCOMPARE(newMsg->to()->asUnicodeString(), msg->to()->asUnicodeString()); + QCOMPARE(newMsg->date()->asUnicodeString(), msg->date()->asUnicodeString()); + QCOMPARE(newMsg->subject()->asUnicodeString(), msg->subject()->asUnicodeString()); + + auto decrypted = newMsg->encodedContent(); + KMime::Message decryptedContent; + decryptedContent.setContent(newMsg->encodedContent()); + decryptedContent.parse(); + KMime::Message expectedContent; + expectedContent.setContent(content); + expectedContent.parse(); + QCOMPARE(decryptedContent.from()->asUnicodeString(), expectedContent.from()->asUnicodeString()); + QCOMPARE(decryptedContent.to()->asUnicodeString(), expectedContent.to()->asUnicodeString()); + QCOMPARE(decryptedContent.date()->asUnicodeString(), expectedContent.date()->asUnicodeString()); + QCOMPARE(decryptedContent.subject()->asUnicodeString(), expectedContent.subject()->asUnicodeString()); + QCOMPARE(decryptedContent.contentType()->asUnicodeString(), expectedContent.contentType()->asUnicodeString()); + QCOMPARE(decryptedContent.encodedBody(), expectedContent.encodedBody()); +} + +QTEST_MAIN(FilterActionDecryptTest) diff --git a/src/filter/autotests/filteractionencrypttest.h b/src/filter/autotests/filteractionencrypttest.h new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractionencrypttest.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef FILTERACTIONENCRYPTTEST_H_ +#define FILTERACTIONENCRYPTTEST_H_ + +#include +#include "gpghelper.h" + +class FilterActionEncryptTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void shouldEncrypt_data(); + void shouldEncrypt(); + +private: + GPGHelper *mGpg = {}; +}; + +#endif diff --git a/src/filter/autotests/filteractionencrypttest.cpp b/src/filter/autotests/filteractionencrypttest.cpp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/filteractionencrypttest.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "filteractionencrypttest.h" +#include "../filteractions/filteractionencrypt.h" + +#include +#include + +void FilterActionEncryptTest::initTestCase() +{ + mGpg = new GPGHelper(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpghome")); + QVERIFY(mGpg->isValid()); +} + +void FilterActionEncryptTest::cleanupTestCase() +{ + delete mGpg; +} + +void FilterActionEncryptTest::shouldEncrypt_data() +{ + QTest::addColumn("key"); + QTest::addColumn("content"); // content for decryption + QTest::addColumn("expected"); // decrypted expected content + + const auto smimeKey = QStringLiteral("SMIME:%1:0FDD972BCEFB5735DC7E8EE57DB7BA4E5FDBE218"); + const auto pgpKey = QStringLiteral("PGP:%1:818AE8DA30F81B0CEA4403BA358732559B8659B2"); + + QDir testDir(QString::fromUtf8(TEST_PATH) + QStringLiteral("/gpgdata")); + const auto tests = testDir.entryInfoList({ QStringLiteral("*.msg") }, QDir::Files, QDir::Name); + for (const auto test : tests) { + QFile plain(test.absoluteFilePath()); + QVERIFY(plain.open(QIODevice::ReadOnly)); + const auto plainData = plain.readAll(); + + QTest::newRow(QStringLiteral("PGP %1").arg(test.baseName()).toUtf8().constData()) + << pgpKey.arg(0) << plainData << plainData; + QTest::newRow(QStringLiteral("SMIME %1").arg(test.baseName()).toUtf8().constData()) + << smimeKey.arg(0) << plainData << plainData; + + QFile smimeFile(test.absoluteFilePath() + QStringLiteral(".smime")); + QVERIFY(smimeFile.open(QIODevice::ReadOnly)); + const auto smimeData = smimeFile.readAll(); + QFile pgpFile(test.absoluteFilePath() + QStringLiteral(".pgp")); + QVERIFY(pgpFile.open(QIODevice::ReadOnly)); + const auto pgpData = pgpFile.readAll(); + + QTest::newRow(QStringLiteral("PGP %1 re-encrypt").arg(test.baseName()).toUtf8().constData()) + << pgpKey.arg(1) << smimeData << plainData; + QTest::newRow(QStringLiteral("SMIME %1 re-encrypt").arg(test.baseName()).toUtf8().constData()) + << smimeKey.arg(1) << pgpData << plainData; + QTest::newRow(QStringLiteral("PGP %1 re-encrypt same key").arg(test.baseName()).toUtf8().constData()) + << pgpKey.arg(1) << pgpData << plainData; + } +} + +void FilterActionEncryptTest::shouldEncrypt() +{ + QFETCH(QString, key); + QFETCH(QByteArray, content); + QFETCH(QByteArray, expected); + + MailCommon::FilterActionEncrypt action(this); + action.argsFromString(key); + QVERIFY(!action.key().isNull()); + QCOMPARE(action.reencrypt(), key.contains(QLatin1String(":1:"))); + + auto msg = KMime::Message::Ptr::create(); + msg->setContent(content); + msg->parse(); + msg->assemble(); + + Akonadi::Item item; + item.setPayload(msg); + + MailCommon::ItemContext context(item, true); + const auto result = action.process(context, false); + QCOMPARE(result, MailCommon::FilterAction::GoOn); + QVERIFY(context.needsPayloadStore()); + + auto newMsg = context.item().payload(); + QCOMPARE(newMsg->from()->asUnicodeString(), msg->from()->asUnicodeString()); + QCOMPARE(newMsg->to()->asUnicodeString(), msg->to()->asUnicodeString()); + QCOMPARE(newMsg->date()->asUnicodeString(), msg->date()->asUnicodeString()); + QCOMPARE(newMsg->subject()->asUnicodeString(), msg->subject()->asUnicodeString()); + + QString gpgexe; + QByteArray resultContent; + GPGHelper::CryptoType crypto; + if (key.startsWith(QLatin1String("PGP"))) { + QCOMPARE(newMsg->contentType()->mimeType(), QByteArray("multipart/encrypted")); + resultContent = newMsg->encodedContent(); + crypto = GPGHelper::OpenPGP; + } else { + QCOMPARE(newMsg->contentType()->mimeType(), QByteArray("application/pkcs7-mime")); + resultContent = QByteArray::fromBase64(newMsg->encodedBody()); + crypto = GPGHelper::SMIME; + } + + // Check if the message is encrypted with the right key + const auto usedKey = mGpg->encryptionKeyFp(resultContent, crypto); + QCOMPARE(usedKey, QString::fromLatin1(action.key().primaryFingerprint())); + + const auto actual = mGpg->decrypt(resultContent, crypto); + + KMime::Message actualContent; + actualContent.setContent(actual); + actualContent.parse(); + KMime::Message expectedContent; + expectedContent.setContent(expected); + expectedContent.parse(); + QCOMPARE(actualContent.from()->asUnicodeString(), expectedContent.from()->asUnicodeString()); + QCOMPARE(actualContent.to()->asUnicodeString(), expectedContent.to()->asUnicodeString()); + QCOMPARE(actualContent.date()->asUnicodeString(), expectedContent.date()->asUnicodeString()); + QCOMPARE(actualContent.subject()->asUnicodeString(), expectedContent.subject()->asUnicodeString()); + QCOMPARE(actualContent.contentType()->asUnicodeString(), expectedContent.contentType()->asUnicodeString()); + QCOMPARE(actualContent.encodedBody(), expectedContent.encodedBody()); +} + +QTEST_MAIN(FilterActionEncryptTest) diff --git a/src/filter/autotests/gpgdata/multipart-alternative.msg b/src/filter/autotests/gpgdata/multipart-alternative.msg new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/multipart-alternative.msg @@ -0,0 +1,18 @@ +From: Daniel Vratil +To: KMail Test +Date: Tue, 01 Aug 2017 07:50:04 +0000 +Subject: It's time to get schwifty! +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="nextPart0" + +--nextPart0 +Content-Type: text/plain + +Wubba-lubba-dub-dub! + +--nextPart0 +Content-Type: text/html + +Wubba-lubba-dub-dub! + +--nextPart0-- diff --git a/src/filter/autotests/gpgdata/multipart-alternative.msg.pgp b/src/filter/autotests/gpgdata/multipart-alternative.msg.pgp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/multipart-alternative.msg.pgp @@ -0,0 +1,34 @@ +From: Daniel Vratil +To: KMail Test +Date: Tue, 01 Aug 2017 07:50:04 +0000 +Subject: It's time to get schwifty! +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="nextPart0"; protocol="application/pgp-encrypted" + +--nextPart0 +Content-Type: application/pgp-encrypted +Content-Disposition: attachment +Content-Transfer-Encoding: 7Bit + +Version: 1 +--nextPart0 +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" +Content-Transfer-Encoding: 7Bit + +-----BEGIN PGP MESSAGE----- + +hQEMA3IXuHpXwGcWAQf/e5YrKiIPaaOU9zLeajnTipkHbGxgP/ZdDfxApfMMeWG+ +r2Ort8PflyvRB4MlFye7PpGTbK3BIFFNzWSlWwhAukbmj0OlJzgAxJZS7OlpX9mk +mae5XkkBnAqqzpwf0d9/JWaOvx4xhzYGjRBoInTxRI+I5iFS1TveQ0j3TsSut6ou +G84zyW2lMWNsXHsl8cGstPsI190LcpoWfdAwW5FTcPr7tqE8JDeNe5BntS3eRx4l +tCvxPQrDtbRdlh6CsMlUjZsRRe1p9l7Ab551YRQFeo+Zi6lP5gnLIImx0KErfiRG +Xcg4VB30jKBj7Q5AhQFCCcKCrLKLZcgs6KUYSb0YPdLAAgEEnoeRmQ6LbUAY9OQb +6+vwM+1vZ6efEkbaBUJwDbFaT9gbdNiS0MyQiNeZav19c/rkIb7JV07ps/d4yU+i +SzqmuwXOv77CjYU+pSmlFvpjVbeZw+YjS7asTEXID6vSE7XOzmjzkjLrJ+tBtBS3 +69dVFfOH/1kb1lFa+iKOkiwCnOo4/LfSGGLvx7ueJuZsqHE8rFIryc6MFtng5HlK +qP+apCdxSkbeG3kxQIC0gljhVQYJkGU25So3H6DyRa2U+B+V +=8G22 +-----END PGP MESSAGE----- + +--nextPart0-- diff --git a/src/filter/autotests/gpgdata/multipart-alternative.msg.smime b/src/filter/autotests/gpgdata/multipart-alternative.msg.smime new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/multipart-alternative.msg.smime @@ -0,0 +1,23 @@ +From: Daniel Vratil +To: KMail Test +Date: Tue, 01 Aug 2017 07:50:04 +0000 +Subject: It's time to get schwifty! +Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="enveloped-data" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7m" +MIME-Version: 1.0 + +MIAGCSqGSIb3DQEHA6CAMIACAQAxggFYMIIBVAIBADA8MDAxCzAJBgNVBAYTAkNa +MQwwCgYDVQQKEwNLREUxEzARBgNVBAMTCktNYWlsIFRlc3QCCDilUKgKqCPGMA0G +CSqGSIb3DQEBAQUABIIBAGqd1mJHm3G29UuwVYZM/TqjLa285LPTAHIqvz86ins+ +7cxsM8uu9R8PgYzpkR6HEeF3X/OY5K4YFj2BO2MS0jDh0u12IFeZSat83QHUCVP8 +hOQYbs6lB6duXrhSVQ/kGVL2l2Us7AWFqHbNzLZTq8sgHySfI5PRgKxmZRi+0/o+ +40zN43azW1RQkcDjxZIQh4etGHbFP/ywBMvW6lShFpz33DFGZzOopdlj834O1aVy +HadFGdqe7oKziQ1xSj05mblHMNeuYHwgB2+5RdemK4y4/BUwu+7LRuUeWjzo3i05 +A3mjz0jUtg6tVLx4sB9cw1Bh5kKL9G5o8FiidFkjAcAwgAYJKoZIhvcNAQcBMB0G +CWCGSAFlAwQBAgQQIRY9qD+NYirOMj+Aa4/JLqCABIHgngiipfaUSmGGW2O/YAdb +xHGYH0GwUwsWxEB2+TkSkLvzxmyXQaUoyLn2h+TJjj4ezEmH3Ir/9YQ85uUj2lpl +/KoXeN3QSsIbWaklyuca32pxSOVb5JWTTj03Kdnotnlg9wU97bcpEJ5pIlrZX4L0 +kTq+WQsK1198kxzhVCmT5LfAVdIMI1YQrRLkW7uZsa83IFMvqvkofGBKHx7lhSst +nil6+bStPyjfPGJChe0UOyhJicBcrm7CNLNiZPtYLBrAEbnSELqsU6a1OO+6wZ9L +l7CvTrvSj6ZV7JY90OhhhCsEENmHHKkBN0X+259Qkhgb1l4AAAAAAAAAAAAA diff --git a/src/filter/autotests/gpgdata/text-plain.msg b/src/filter/autotests/gpgdata/text-plain.msg new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/text-plain.msg @@ -0,0 +1,8 @@ +From: Daniel Vratil +To: KMail Test +Date: Tue, 01 Aug 2017 07:50:04 +0000 +Subject: It's time to get schwifty! +Content-Type: text/plain +MIME-Version: 1.0 + +Show me what you got! diff --git a/src/filter/autotests/gpgdata/text-plain.msg.pgp b/src/filter/autotests/gpgdata/text-plain.msg.pgp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/text-plain.msg.pgp @@ -0,0 +1,32 @@ +From: Daniel Vratil +To: KMail Test +Date: Tue, 01 Aug 2017 07:50:04 +0000 +Subject: It's time to get schwifty! +Content-Type: multipart/encrypted; boundary="nextPart"; protocol="application/pgp-encrypted" +MIME-Version: 1.0 + +--nextPart +Content-Type: application/pgp-encrypted +Content-Disposition: attachment +Content-Transfer-Encoding: 7Bit + +Version: 1 +--nextPart +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" +Content-Transfer-Encoding: 7Bit + +-----BEGIN PGP MESSAGE----- + +hQEMA3IXuHpXwGcWAQf/dYk2TJpb6J6Ji9mA4RzMr5CUyiMkDlOe6+l4BVtRHsLm +g9Bwe4znqCenyf/B6MdGThPAFvYRcN2UPbLEElrZQ5vsCyYAG8sespy3sVQChA1H +063pLvYZDxV/FdA2ckduaxQND9kNdfSONn7+olTaKgDiKA1oSgZ+dvRHG5cr/xQ+ +AoYA/iQ65dT3MQyvWF1Iw+AKrbhR8XRfB77Hl+qL/vr62pbnVVA2rHjEu1Tjz04I +6oS55zoetonBVYiSIpQLRu5Uu0ys8jUY29qGgwd/VlBB2Y/YcYY3OTRbkvYBJ2Yd +hc1ca4YCEaJ/9e8BuhLfBzh4J9EZgJ3NYRTm3FZRadJrAftlxAnsG1HmvE3kNV6Q +zHWxlIQZQE+sSr7dbnpyObpHaiAaU1toaH22uKRh+9ZBhhiNYhCHk1KnkUKG7gdR +Wys2qPAhuMVZryoW0uQyX4lA5ZjsetBSxUVffjjHZKYP56dyLzpvMGqWvK0= +=DWvh +-----END PGP MESSAGE----- + +--nextPart-- diff --git a/src/filter/autotests/gpgdata/text-plain.msg.smime b/src/filter/autotests/gpgdata/text-plain.msg.smime new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpgdata/text-plain.msg.smime @@ -0,0 +1,21 @@ +From: Daniel Vratil +To: KMail Test +Date: Tue, 01 Aug 2017 07:50:04 +0000 +Subject: It's time to get schwifty! +Content-Type: application/pkcs7-mime; name="smime.p7m"; smime-type="enveloped-data" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7m" +MIME-Version: 1.0 + +MIAGCSqGSIb3DQEHA6CAMIACAQAxggFYMIIBVAIBADA8MDAxCzAJBgNVBAYTAkNa +MQwwCgYDVQQKEwNLREUxEzARBgNVBAMTCktNYWlsIFRlc3QCCDilUKgKqCPGMA0G +CSqGSIb3DQEBAQUABIIBAIGrbP6RHuRHdMhJpXuyRjzWPdApinxmUBtvPCxwfLK7 +0ZHa11jAwJbIvouuqblReZX/eCYbBYsgW5aSFn1dSV/lUQ49gOTRHnRXEBrQGK4z +gdNK+g+axqZO4zUtKyEDpKi7bnS2i/aCi5Q61PWFSkjgucrUuxyGP82iK5WQpcuk +0c7xR8cxaZf/tpQGnFRZ6OlUVLi2iSbv+ewbrZnaQgu3XAKQcJHrZlArgtR3cneA +X8vpCDE5R/DpZ9onJLwd/KI1EaoEjeZoa6VMVm324uxG2vAaubvg9p4lBR0Q69hL +MIpt8KGgz4psbAiE838U5jh77o5lxQmviolmuyEtZXswgAYJKoZIhvcNAQcBMB0G +CWCGSAFlAwQBAgQQrA0/KpKc4c8gmw1PZUNJXqCABDAgX5OMUtnouELP2pOKz4TN +pLHtscYX9+1XD4Rn+hqPOZ7aytiFOvpdn2qUqHtqhT8EEDumRT2M6mQ6rouGaXk7 +DDAAAAAAAAAAAAAA + diff --git a/src/filter/autotests/gpghelper.h b/src/filter/autotests/gpghelper.h new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpghelper.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef GPGHELPER_H_ +#define GPGHELPER_H_ + +#include +#include + +class GPGHelper +{ +public: + enum CryptoType { + OpenPGP, + SMIME + }; + + explicit GPGHelper(const QString &templateGnupgHome); + ~GPGHelper(); + + bool isValid() const { return mValid; } + QString gnupgHome() const; + + QByteArray decrypt(const QByteArray &enc, CryptoType crypto) const; + QByteArray encrypt(const QByteArray &dec, CryptoType crypto) const; + + QString encryptionKeyFp(const QByteArray &encMsg, GPGHelper::CryptoType crypto) const; + +private: + QByteArray runGpg(const QByteArray &in, CryptoType crypt, const QStringList &args) const; + + bool mValid; + QTemporaryDir mTmpDir; +}; + +#endif + diff --git a/src/filter/autotests/gpghelper.cpp b/src/filter/autotests/gpghelper.cpp new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpghelper.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License, version 2, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "gpghelper.h" + +#include +#include +#include +#include + +namespace { + +bool copyRecursively(const QString &src, const QString &dest) +{ + QFileInfo srcInfo(src); + if (srcInfo.isDir()) { + QDir destDir(dest); + destDir.cdUp(); + if (!destDir.mkdir(QFileInfo(src).fileName())) { + qWarning() << "Failed to create directory" << QFileInfo(src).fileName() << "in" << destDir.path(); + return false; + } + QDir srcDir(src); + const auto srcFiles = srcDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System); + for (const auto &fileName : srcFiles) { + const QString srcFile = src + QLatin1Char('/') + fileName; + const QString dstFile = dest + QLatin1Char('/') + fileName; + if (!copyRecursively(srcFile, dstFile)) { + return false; + } + } + } else { + if (!QFile::copy(src, dest)) { + qWarning() << "Failed to copy" << src << "into" << dest; + return false; + } + } + return true; +} + +QString gpgexe(GPGHelper::CryptoType crypto) +{ + return (crypto == GPGHelper::OpenPGP) ? QStringLiteral("gpg2") : QStringLiteral("gpgsm"); +} + +} // namespace + +GPGHelper::GPGHelper(const QString &templateGnupgHome) + : mValid(false) +{ + const auto home = gnupgHome(); + mValid = copyRecursively(templateGnupgHome, home); + if (mValid) { + qputenv("GNUPGHOME", home.toUtf8()); + } +} + +GPGHelper::~GPGHelper() +{ + // shutdown gpg-agent + QProcess gpgshutdown; + auto env = gpgshutdown.processEnvironment(); + env.insert("GNUPGHOME", gnupgHome()); + gpgshutdown.setProcessEnvironment(env); + gpgshutdown.start(QStringLiteral("gpg-connect-agent")); + QVERIFY(gpgshutdown.waitForStarted()); + gpgshutdown.write("KILLAGENT"); + gpgshutdown.closeWriteChannel(); + QVERIFY(gpgshutdown.waitForFinished()); +} + +QString GPGHelper::gnupgHome() const +{ + return mTmpDir.path() + QStringLiteral("/gpghome"); +} + +QByteArray GPGHelper::runGpg(const QByteArray &in, GPGHelper::CryptoType crypto, + const QStringList &args) const +{ + QProcess gpg; + gpg.setReadChannel(QProcess::StandardOutput); + auto env = gpg.processEnvironment(); + env.insert("GNUPGHOME", gnupgHome()); + gpg.setProcessEnvironment(env); + gpg.start(gpgexe(crypto), args); + if (!gpg.waitForStarted()) { + return {}; + } + gpg.write(in); + gpg.closeWriteChannel(); + if (!gpg.waitForReadyRead()) { + return {}; + } + const auto out = gpg.readAllStandardOutput(); + + if (!gpg.waitForFinished()) { + return {}; + } + + return out; +} + +QByteArray GPGHelper::decrypt(const QByteArray &enc, GPGHelper::CryptoType crypto) const +{ + return runGpg(enc, crypto, { QStringLiteral("-d") }); +} + +QByteArray GPGHelper::encrypt(const QByteArray &dec, GPGHelper::CryptoType crypto) const +{ + return runGpg(dec, crypto, { QStringLiteral("-e") }); +} + +QString GPGHelper::encryptionKeyFp(const QByteArray &enc, GPGHelper::CryptoType crypto) const +{ + const auto data = runGpg(enc, crypto, { QStringLiteral("--fingerprint"), + QStringLiteral("--with-colons") }); + int idx = data.indexOf("\nfpr:"); + if (idx == -1) { + return {}; + } + + // Find first non-colon character after "fpr" + for (idx = idx + 4; idx < data.size() && data[idx] == ':'; ++idx); + const int end = data.indexOf(':', idx); + + return QString::fromLatin1(data.constData() + idx, end - idx); +} diff --git a/src/filter/autotests/gpghome/.gpg-v21-migrated b/src/filter/autotests/gpghome/.gpg-v21-migrated new file mode 100644 diff --git a/src/filter/autotests/gpghome/openpgp-revocs.d/818AE8DA30F81B0CEA4403BA358732559B8659B2.rev b/src/filter/autotests/gpghome/openpgp-revocs.d/818AE8DA30F81B0CEA4403BA358732559B8659B2.rev new file mode 100644 --- /dev/null +++ b/src/filter/autotests/gpghome/openpgp-revocs.d/818AE8DA30F81B0CEA4403BA358732559B8659B2.rev @@ -0,0 +1,32 @@ +This is a revocation certificate for the OpenPGP key: + +pub rsa2048 2017-08-01 [SC] [expires: 2019-08-01] + 818AE8DA30F81B0CEA4403BA358732559B8659B2 +uid KMail Test + +A revocation certificate is a kind of "kill switch" to publicly +declare that a key shall not anymore be used. It is not possible +to retract such a revocation certificate once it has been published. + +Use it to revoke this key in case of a compromise or loss of +the secret key. However, if the secret key is still accessible, +it is better to generate a new revocation certificate and give +a reason for the revocation. For details see the description of +of the gpg command "--generate-revocation" in the GnuPG manual. + +To avoid an accidental use of this file, a colon has been inserted +before the 5 dashes below. Remove this colon with a text editor +before importing and publishing this revocation certificate. + +:-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: This is a revocation certificate + +iQE2BCABCAAgFiEEgYro2jD4GwzqRAO6NYcyVZuGWbIFAlmAfNQCHQAACgkQNYcy +VZuGWbJ5hQgAkNopkhsaO0yyu+xs3AymBzha/YDAqQLMEH6b3URyTyF7iyYD1HbG +fwYN/PcH5H6xreSTQiQoopER7hB0H42P1ASRyDyRxTk4EuiOfrgkewa8H+jCe6OK +HPZ3c1g6zo7LZFhIldXZ0aOVaHEuqldWnWJGhxfN2ew4xNUM+7yrA1N4WDRBzI/3 +CRzr+sG2wMZKixlpKqthvpH8doGFiNLpY4KmiKh9rjQ5f2KLSGcDDTNXsinRwOrr +jXIV4Y9u0ZugzmgWHlWV6bYX5ChsfI5OzjMCrCm+XuCsdFMca3tArQh/6KOY4zjW +L+iFSgdqd4gewSvpY8FTtKzkRlX2HMpYhg== +=rzUF +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/filter/autotests/gpghome/private-keys-v1.d/1AB79D3867F2789661A1FF16675F5778804FD2AD.key b/src/filter/autotests/gpghome/private-keys-v1.d/1AB79D3867F2789661A1FF16675F5778804FD2AD.key new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef MAILCOMMON_FILTERACTION_DECRYPT_H_ +#define MAILCOMMON_FILTERACTION_DECRYPT_H_ + +#include "filteractionwithcrypto.h" + + +namespace MailCommon { + +class FilterActionDecrypt : public FilterActionWithCrypto +{ + Q_OBJECT +public: + explicit FilterActionDecrypt(QObject *parent = nullptr); + ~FilterActionDecrypt() override; + + static FilterAction *newAction(); + + QString displayString() const override; + + QString argsAsString() const override; + void argsFromString(const QString &argsStr) override; + + SearchRule::RequiredPart requiredPart() const override; + FilterAction::ReturnCode process(ItemContext &context, bool applyOnOutbound) const override; +}; + +} // namespace MailCommon + +#endif diff --git a/src/filter/filteractions/filteractiondecrypt.cpp b/src/filter/filteractions/filteractiondecrypt.cpp new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractiondecrypt.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "filteractiondecrypt.h" +#include "mailcommon_debug.h" + +#include + +#include + +#include +#include +#include + +#include + +#include + +using namespace MailCommon; + +FilterActionDecrypt::FilterActionDecrypt(QObject *parent) + : FilterActionWithCrypto(QStringLiteral("decrypt"), i18n("Decrypt"), parent) +{ +} + +FilterActionDecrypt::~FilterActionDecrypt() +{ +} + +FilterAction *FilterActionDecrypt::newAction() +{ + return new FilterActionDecrypt(); +} + +QString FilterActionDecrypt::displayString() const +{ + return i18n("Decrypt"); +} + +QString FilterActionDecrypt::argsAsString() const +{ + return {}; +} + +void FilterActionDecrypt::argsFromString(const QString &) +{ +} + +SearchRule::RequiredPart FilterActionDecrypt::requiredPart() const +{ + return SearchRule::CompleteMessage; +} + +FilterAction::ReturnCode FilterActionDecrypt::process(ItemContext &context, bool) const +{ + auto &item = context.item(); + if (!item.hasPayload()) { + return ErrorNeedComplete; + } + + auto msg = item.payload(); + if (!KMime::isEncrypted(msg.data())) { + return GoOn; + } + + bool wasEncrypted; + auto nec = decryptMessage(msg, wasEncrypted); + if (!nec) { + return wasEncrypted ? ErrorButGoOn: GoOn; + } + + context.item().setPayload(nec); + context.item().clearFlag(Akonadi::MessageFlags::Encrypted); + context.setNeedsPayloadStore(); + context.setNeedsFlagStore(); + return GoOn; +} diff --git a/src/filter/filteractions/filteractiondict.cpp b/src/filter/filteractions/filteractiondict.cpp --- a/src/filter/filteractions/filteractiondict.cpp +++ b/src/filter/filteractions/filteractiondict.cpp @@ -23,8 +23,10 @@ #include "filteractionaddtag.h" #include "filteractionaddtoaddressbook.h" #include "filteractioncopy.h" +#include "filteractiondecrypt.h" #include "filteractiondelete.h" #include "filteractionexec.h" +#include "filteractionencrypt.h" #include "filteractionforward.h" #include "filteractionmove.h" #include "filteractionpipethrough.h" @@ -74,6 +76,8 @@ insert(FilterActionAddToAddressBook::newAction); insert(FilterActionDelete::newAction); insert(FilterActionUnsetStatus::newAction); + insert(FilterActionEncrypt::newAction); + insert(FilterActionDecrypt::newAction); // Register custom filter actions below this line. } diff --git a/src/filter/filteractions/filteractionencrypt.h b/src/filter/filteractions/filteractionencrypt.h new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractionencrypt.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef MAILCOMMON_FILTERACTION_ENCRYPT_H_ +#define MAILCOMMON_FILTERACTION_ENCRYPT_H_ + +#include "filteractionwithcrypto.h" + +#include + +#include + +namespace MailCommon { + +class FilterActionEncrypt : public FilterActionWithCrypto +{ + Q_OBJECT +public: + explicit FilterActionEncrypt(QObject *parent = nullptr); + ~FilterActionEncrypt() override; + + static FilterAction *newAction(); + + QString displayString() const override; + + QString argsAsString() const override; + void argsFromString(const QString &argsStr) override; + + SearchRule::RequiredPart requiredPart() const override; + FilterAction::ReturnCode process(ItemContext &context, bool applyOnOutbound) const override; + + bool isEmpty() const override; + + QString informationAboutNotValidAction() const override; + + QWidget *createParamWidget(QWidget *parent) const override; + void setParamWidgetValue(QWidget *paramWidget) const override; + void applyParamWidgetValue(QWidget *paramWidget) override; + + GpgME::Key key() const { return mKey; } + bool reencrypt() const { return mReencrypt; } +private: + std::shared_ptr mKeyCache; + GpgME::Key mKey; + bool mReencrypt; +}; + +} // namespace MailCommon + +#endif diff --git a/src/filter/filteractions/filteractionencrypt.cpp b/src/filter/filteractions/filteractionencrypt.cpp new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractionencrypt.cpp @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "filteractionencrypt.h" +#include "mailcommon_debug.h" + +#include +#include +#include +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#include + +using namespace MailCommon; + +#define LISTING_FINISHED "listingFinished" + +FilterActionEncrypt::FilterActionEncrypt(QObject *parent) + : FilterActionWithCrypto(QStringLiteral("encrypt"), i18n("Encrypt"), parent) + , mKeyCache(Kleo::KeyCache::instance()) +{ +} + +FilterActionEncrypt::~FilterActionEncrypt() +{ +} + +FilterAction *FilterActionEncrypt::newAction() +{ + return new FilterActionEncrypt(); +} + +QString FilterActionEncrypt::displayString() const +{ + return label(); +} + +QString FilterActionEncrypt::argsAsString() const +{ + if (mKey.isNull()) { + return {}; + } + + const auto proto = ((mKey.protocol() == GpgME::OpenPGP) ? QStringLiteral("PGP") + : QStringLiteral("SMIME")); + return QStringLiteral("%1:%2:%3").arg(proto, QString::number(int(mReencrypt)), + QString::fromLatin1(mKey.primaryFingerprint())); +} + +void FilterActionEncrypt::argsFromString(const QString &argsStr) +{ + const int pos = argsStr.indexOf(QLatin1Char(':')); + const auto protoStr = argsStr.leftRef(pos); + + QGpgME::Protocol *proto = {}; + if (protoStr == QLatin1String("PGP")) { + proto = QGpgME::openpgp(); + } else if (protoStr == QLatin1String("SMIME")) { + proto = QGpgME::smime(); + } else { + qCWarning(MAILCOMMON_LOG) << "Unknown protocol specified:" << protoStr; + return; + } + + mReencrypt = static_cast(argsStr.midRef(pos + 1, 1).toInt()); + + const auto fp = argsStr.mid(pos + 3); + auto listJob = proto->keyListJob(false, true, true); + + std::vector keys; + auto result = listJob->exec({ fp }, true, keys); + listJob->deleteLater(); + + if (result.error()) { + qCWarning(MAILCOMMON_LOG) << "Failed to retrieve keys:" << result.error().asString(); + return; + } + + if (keys.empty()) { + qCWarning(MAILCOMMON_LOG) << "Could not obtain configured key: key expired or removed?"; + // TODO: Interactively ask user to re-configure the filter + return; + } + + mKey = keys[0]; +} + +SearchRule::RequiredPart FilterActionEncrypt::requiredPart() const +{ + return SearchRule::CompleteMessage; +} + +FilterAction::ReturnCode FilterActionEncrypt::process(ItemContext &context, bool) const +{ + if (mKey.isNull()) { + qCWarning(MAILCOMMON_LOG) << "FilterActionEncrypt::process called without filter having a key!"; + return ErrorButGoOn; + } + + auto &item = context.item(); + if (!item.hasPayload()) { + qCWarning(MAILCOMMON_LOG) << "Item" << item.id() << "does not contain KMime::Message payload!"; + return ErrorNeedComplete; + } + + auto msg = item.payload(); + if (KMime::isEncrypted(msg.data())) { + if (mReencrypt) { + // Make sure the email is not already encrypted by the mKey - this is + // a little expensive, but still much cheaper than modifying and + // re-uploading the email to the server + const auto encryptionKeys = getEncryptionKeysFromContent(msg, mKey.protocol()); + qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "encrypted by following keys: " << encryptionKeys; + if (!encryptionKeys.isEmpty()) { + if (mKey.protocol() == GpgME::OpenPGP) { + std::vector ids; + ids.reserve(encryptionKeys.size()); + for (const auto key : encryptionKeys) { + ids.push_back(key.toStdString()); + } + for (const auto key : mKeyCache->findByKeyIDOrFingerprint(ids)) { + if (qstrcmp(key.primaryFingerprint(), mKey.primaryFingerprint()) == 0) { + // This email is already encrypted with the target key, + // so there's no need to re-encrypt it + qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting"; + return GoOn; + } + } + } else if (mKey.protocol() == GpgME::CMS) { + // We are only able to get serial + for (const auto key : mKeyCache->secretKeys()) { + if (qstrcmp(key.issuerSerial(), mKey.issuerSerial()) == 0) { + // This email is already encrypted with the target key, + // so there's no need to re-encrypt it + qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting"; + return GoOn; + } + } + } + } + bool dummy; // dummy + const auto decrypted = decryptMessage(msg, dummy); + if (!decrypted) { + // We failed to decrypt the encrypted email - very likely we just don't + // have the right key, so don't consider it an error + return GoOn; + } else { + msg = decrypted; + } + } else { + return GoOn; + } + } + + MessageComposer::EncryptJob encrypt; + encrypt.setContent(msg.data()); + encrypt.setCryptoMessageFormat(mKey.protocol() == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat); + encrypt.setEncryptionKeys({ mKey }); + encrypt.exec(); + if (encrypt.error()) { + qCWarning(MAILCOMMON_LOG) << "Encryption error:" << encrypt.errorString(); + return ErrorButGoOn; + } + + KMime::Content *result = encrypt.content(); + result->assemble(); + + auto nec = assembleMessage(msg, result); + context.item().setPayload(nec); + context.item().setFlag(Akonadi::MessageFlags::Encrypted); + context.setNeedsPayloadStore(); + context.setNeedsFlagStore(); + + delete result; + + return GoOn; +} + +bool FilterActionEncrypt::isEmpty() const +{ + return mKey.isNull(); +} + +QString FilterActionEncrypt::informationAboutNotValidAction() const +{ + return i18n("No encryption key has been selected"); +} + +QWidget *FilterActionEncrypt::createParamWidget(QWidget *parent) const +{ + auto w = new QWidget(parent); + auto l = new QVBoxLayout; + w->setLayout(l); + + auto combo = new Kleo::KeySelectionCombo(w); + combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint())); + + std::shared_ptr filter(new Kleo::DefaultKeyFilter); + filter->setIsOpenPGP(Kleo::DefaultKeyFilter::DoesNotMatter); + filter->setCanEncrypt(Kleo::DefaultKeyFilter::Set); + filter->setHasSecret(Kleo::DefaultKeyFilter::Set); + combo->setKeyFilter(filter); + + combo->setProperty(LISTING_FINISHED, false); + connect(combo, &Kleo::KeySelectionCombo::keyListingFinished, + combo, [combo] { + combo->setProperty(LISTING_FINISHED, true); + }); + connect(combo, &Kleo::KeySelectionCombo::currentKeyChanged, + this, &FilterActionEncrypt::filterActionModified); + l->addWidget(combo); + + auto chkBox = new QCheckBox(w); + chkBox->setText(i18n("Re-encrypt encrypted emails with this key")); + chkBox->setChecked(mReencrypt); + l->addWidget(chkBox); + + auto lbl = new QLabel(w); + auto palette = lbl->palette(); + palette.setColor(lbl->foregroundRole(), KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText).color()); + lbl->setPalette(palette); + lbl->setWordWrap(true); + lbl->setText(i18n("Warning: the encrypted emails will be uploaded back to the server!")); + lbl->setToolTip(i18n("

You will not be able to read the encrypted emails on any other computer " + "or email client unless you have your private key available there.

" + "

Also note that most webmail interfaces don't support encryption, so you " + "will not be able to read the encrypted emails there.

")); + l->addWidget(lbl); + + return w; +} + +void FilterActionEncrypt::setParamWidgetValue(QWidget *paramWidget) const +{ + if (auto combo = paramWidget->findChild()) { + combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint())); + combo->setCurrentKey(QString::fromLatin1(mKey.primaryFingerprint())); + } + if (auto chkBox = paramWidget->findChild()) { + chkBox->setChecked(mReencrypt); + } +} + +void FilterActionEncrypt::applyParamWidgetValue(QWidget *paramWidget) +{ + if (auto combo = paramWidget->findChild()) { + // FIXME: This is super-ugly, but unfortunately the filtering code generates + // several instances of this filter and passes the paramWidgets from one + // instance to another to "copy" stuff in between, which in our case leads + // to this method being called on an un-populated combobox + if (!combo->property(LISTING_FINISHED).toBool()) { + QEventLoop ev; + connect(combo, &Kleo::KeySelectionCombo::keyListingFinished, + &ev, &QEventLoop::quit, Qt::QueuedConnection); + ev.exec(); + } + mKey = combo->currentKey(); + } + if (auto chkBox = paramWidget->findChild()) { + mReencrypt = chkBox->isChecked(); + } +} diff --git a/src/filter/filteractions/filteractionwithcrypto.h b/src/filter/filteractions/filteractionwithcrypto.h new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractionwithcrypto.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef MAILCOMMON_FILTERACTION_WITH_CRYPTO_H_ +#define MAILCOMMON_FILTERACTION_WITH_CRYPTO_H_ + +#include "filteraction.h" + +#include + +namespace MailCommon { + +class FilterActionWithCrypto : public FilterAction +{ + Q_OBJECT + +protected: + using FilterAction::FilterAction; + + KMime::Message::Ptr assembleMessage(const KMime::Message::Ptr &orig, + const KMime::Content *newContent) const; + KMime::Message::Ptr decryptMessage(const KMime::Message::Ptr &decrypt, + bool &wasEncrypted) const; + + bool isPGP(const KMime::Content *content, bool allowOctetStream = false) const; + bool isSMIME(const KMime::Content *content) const; + + QStringList getEncryptionKeysFromContent(const KMime::Message::Ptr &msg, GpgME::Protocol proto) const; + +private: + void copyHeader(const KMime::Headers::Base *header, + KMime::Message::Ptr destMsg) const; + bool isContentHeader(const KMime::Headers::Base *header) const; + +private: + // cached values + mutable QString mGpgSmPath; + mutable QString mGpgPath; +}; + +} // namespace MailCommon + +#endif diff --git a/src/filter/filteractions/filteractionwithcrypto.cpp b/src/filter/filteractions/filteractionwithcrypto.cpp new file mode 100644 --- /dev/null +++ b/src/filter/filteractions/filteractionwithcrypto.cpp @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2017 Daniel Vrátil + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "filteractionwithcrypto.h" +#include "mailcommon_debug.h" + +#include +#include + +#include + +#include +#include +#include + +using namespace MailCommon; + +bool FilterActionWithCrypto::isPGP(const KMime::Content *part, bool allowOctetStream) const +{ + 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 FilterActionWithCrypto::isSMIME(const KMime::Content *part) const +{ + const auto ct = static_cast(part->headerByType("Content-Type")); + return ct && (ct->isSubtype("pkcs7-mime") || ct->isSubtype("x-pkcs7-mime")); +} + + +KMime::Message::Ptr FilterActionWithCrypto::decryptMessage(const KMime::Message::Ptr &msg, + bool &wasEncrypted) const +{ + QGpgME::Protocol *proto = {}; + if (msg->mainBodyPart("multipart/encrypted")) { + const auto subparts = msg->contents(); + for (auto subpart : subparts) { + if (isPGP(subpart, true)) { + proto = QGpgME::openpgp(); + break; + } else if (isSMIME(subpart)) { + proto = QGpgME::smime(); + break; + } + } + } else { + if (isPGP(msg.data())) { + proto = QGpgME::openpgp(); + } else if (isSMIME(msg.data())) { + proto = QGpgME::smime(); + } + } + + if (!proto) { + // Not encrypted, or we don't recognize the encryption + wasEncrypted = false; + return {}; + } + + wasEncrypted = true; + QByteArray outData, inData; + if (proto == QGpgME::smime()) { + inData = QByteArray::fromBase64(msg->encodedBody()); + } else { + inData = msg->encodedContent(); + } + auto decrypt = proto->decryptJob(); + auto result = decrypt->exec(inData, outData); + if (result.error()) { + // unknown key, invalid algo, or general error + qCWarning(MAILCOMMON_LOG) << "Failed to decrypt:" << result.error().asString(); + return {}; + } + + KMime::Content decCt; + decCt.setContent(outData); + decCt.parse(); + decCt.assemble(); + + return assembleMessage(msg, &decCt); +} + +void FilterActionWithCrypto::copyHeader(const KMime::Headers::Base *header, + KMime::Message::Ptr msg) const +{ + auto newHdr = KMime::Headers::createHeader(header->type()); + if (!newHdr) { + newHdr = new KMime::Headers::Generic(header->type()); + } + newHdr->fromUnicodeString(header->asUnicodeString(), "UTF-8"); + msg->appendHeader(newHdr); +} + +bool FilterActionWithCrypto::isContentHeader(const KMime::Headers::Base *header) const +{ + return header->is("Content-Type") + || header->is("Content-Transfer-Encoding") + || header->is("Content-Disposition"); +} + +KMime::Message::Ptr FilterActionWithCrypto::assembleMessage(const KMime::Message::Ptr &orig, + const KMime::Content *newContent) const +{ + auto out = KMime::Message::Ptr::create(); + // Use the new content as message content + out->setBody(const_cast(newContent)->encodedBody()); + out->parse(); + + // Copy over headers from the original message, except for CT, CTE and CD + // headers, we want to preserve those from the new content + QVector headers = orig->headers(); + for (const auto hdr : qAsConst(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 : qAsConst(headers)) { + if (isContentHeader(hdr)) { + copyHeader(hdr, out); + } + } + + out->assemble(); + out->parse(); + + return out; +} + +QStringList FilterActionWithCrypto::getEncryptionKeysFromContent(const KMime::Message::Ptr &msg, + GpgME::Protocol protocol) const +{ + if (protocol == GpgME::CMS && mGpgSmPath.isNull()) { + auto path = QStandardPaths::findExecutable(QStringLiteral("gpgsm")); + mGpgSmPath = path.isEmpty() ? QStringLiteral("") : path; + } else if (protocol == GpgME::OpenPGP && mGpgPath.isNull()) { + auto path = QStandardPaths::findExecutable(QStringLiteral("gpg2")); + if (path.isEmpty()) { + path = QStandardPaths::findExecutable(QStringLiteral("gpg")); + mGpgPath = path.isEmpty() ? QStringLiteral("") : path; + } else { + mGpgPath = path; + } + } + + if ((protocol == GpgME::CMS && mGpgSmPath.isEmpty()) + || (protocol == GpgME::OpenPGP && mGpgPath.isEmpty())) { + return {}; + } + + QProcess gpg; + QStringList keyIds; + // TODO: contribute an API for this into gpgme + if (protocol == GpgME::OpenPGP) { + gpg.setProgram(mGpgPath); + // --list-packets will give us list of keys used to encrypt the message + // --batch will prevent gpg from asking for decryption password (we don't need it yet) + gpg.setArguments({ QStringLiteral("--list-packets"), QStringLiteral("--batch") }); + gpg.start(QIODevice::ReadWrite); + gpg.waitForStarted(); + gpg.write(msg->encodedContent()); + gpg.closeWriteChannel(); + gpg.waitForFinished(); + while (!gpg.atEnd()) { + const auto l = gpg.readLine(); + if (l.startsWith(":pubkey")) { + const int pos = l.indexOf("keyid "); + if (pos < 0) { + continue; + } + const int start = pos + 6; // strlen("keyid ") + const int len = l.size() - start - 1; // -1 to skip trailing \n + keyIds << QString::fromUtf8(l.mid(start, len)); + } + } + } else if (protocol == GpgME::CMS) { + gpg.setProgram(mGpgSmPath); + // --decrypt - the only way how to get the keys from gpgsm, sadly, is to decrypt the email + // --status-fd 2 - make sure the status output is not mangled with the decrypted content + // --assume-base64 - so that we don't have to decode it ourselves + gpg.setArguments({ QStringLiteral("--decrypt"), + QStringLiteral("--status-fd"), QStringLiteral("2"), + QStringLiteral("--debug-level"), QStringLiteral("basic"), + QStringLiteral("--assume-base64") }); + gpg.start(QIODevice::ReadWrite); + gpg.waitForStarted(); + gpg.write(msg->encodedBody()); // just the body! + gpg.closeWriteChannel(); + gpg.waitForFinished(); + gpg.setReadChannel(QProcess::StandardError); + while (!gpg.atEnd()) { + const auto l = gpg.readLine(); + if (l.startsWith("gpgsm: DBG: recp ")) { + const int pos = l.indexOf("serial: "); + if (pos < 0) { + continue; + } + const int start = pos + 8; // strlen("serial: ") + const int len = l.size() - start - 1; // -1 to skip trailing \n + keyIds << QString::fromUtf8(l.mid(start, len)); + } + } + } + + return keyIds; +} +