Commit 4bfdadca authored by Sandro Knauß's avatar Sandro Knauß
Browse files

Implement of "Protected Headers for Cryptographic E-mail". T742

Summary:
Currently protected headers are in draft status for an RFC:
https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/

Test Plan: run autotests successfully

Subscribers: kde-pim

Tags: #kde_pim

Differential Revision: https://phabricator.kde.org/D28448
parent d3fa256d
Content-Type: text/plain
one flew over the cuckoo's nest
Content-Type: text/plain; protected-headers="v1"
To: to@test.de, to2@test.de
Cc: cc@test.de, cc2@test.de
Subject: =?UTF-8?B?YXNkZmdoamtsw7Y=?=
one flew over the cuckoo's nest
Content-Type: multipart/mixed; boundary="123456789"; protected-headers="v1"
To: to@test.de, to2@test.de
Cc: cc@test.de, cc2@test.de
Subject: =?UTF-8?B?YXNkZmdoamtsw7Y=?=
--123456789
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="UTF-8"; protected-headers="v1"
Subject: asdfghjkl=C3=B6
--123456789
Content-Type: text/plain
one flew over the cuckoo's nest
--123456789--
......@@ -30,21 +30,32 @@
#include <MessageComposer/Composer>
#include <MessageComposer/EncryptJob>
#include <MessageComposer/GlobalPart>
#include <MessageComposer/MainTextJob>
#include <MessageComposer/TextPart>
#include <MessageComposer/GlobalPart>
#include <MessageComposer/TransparentJob>
#include <MessageComposer/Util>
#include <MimeTreeParser/ObjectTreeParser>
#include <MimeTreeParser/NodeHelper>
#include <QGpgME/Protocol>
#include <QGpgME/DecryptVerifyJob>
#include <gpgme++/verificationresult.h>
#include <gpgme++/decryptionresult.h>
#include <stdlib.h>
#include <KCharsets>
#include <QDebug>
#include <QTest>
#include <decryptionresult.h>
QTEST_MAIN(EncryptJobTest)
using namespace MessageComposer;
void EncryptJobTest::initTestCase()
{
MessageComposer::Test::setupEnv();
......@@ -102,7 +113,6 @@ void EncryptJobTest::testContentChained()
VERIFYEXEC(mainTextJob);
std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys();
qDebug() << "done getting keys";
MessageComposer::EncryptJob *eJob = new MessageComposer::EncryptJob(composer);
QStringList recipients;
......@@ -116,6 +126,35 @@ void EncryptJobTest::testContentChained()
checkEncryption(eJob);
}
void EncryptJobTest::testContentSubjobChained()
{
std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys();
QByteArray data(QString::fromLocal8Bit("one flew over the cuckoo's nest").toUtf8());
KMime::Message skeletonMessage;
KMime::Content *content = new KMime::Content;
content->contentType(true)->setMimeType("text/plain");
content->setBody(data);
auto tJob = new TransparentJob;
tJob->setContent(content);
QStringList recipients;
recipients << QString::fromLocal8Bit("test@kolab.org");
Composer composer;
auto eJob = new MessageComposer::EncryptJob(&composer);
eJob->setCryptoMessageFormat(Kleo::OpenPGPMIMEFormat);
eJob->setRecipients(recipients);
eJob->setEncryptionKeys(keys);
eJob->setSkeletonMessage(&skeletonMessage);
eJob->appendSubjob(tJob);
checkEncryption(eJob);
}
void EncryptJobTest::testHeaders()
{
std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys();
......@@ -151,6 +190,95 @@ void EncryptJobTest::testHeaders()
QCOMPARE(result->contentTransferEncoding()->encoding(), KMime::Headers::CE7Bit);
}
void EncryptJobTest::testProtectedHeaders_data()
{
QTest::addColumn<bool>("protectedHeaders");
QTest::addColumn<bool>("protectedHeadersObvoscate");
QTest::addColumn<QString>("referenceFile");
QTest::newRow("simple-obvoscate") << true << true << QStringLiteral("protected_headers-obvoscate.mbox");
QTest::newRow("simple-non-obvoscate") << true << false << QStringLiteral("protected_headers-non-obvoscate.mbox");
QTest::newRow("non-protected_headers") << false << false << QStringLiteral("non-protected_headers.mbox");
}
void EncryptJobTest::testProtectedHeaders()
{
QFETCH(bool,protectedHeaders);
QFETCH(bool, protectedHeadersObvoscate);
QFETCH(QString, referenceFile);
std::vector< GpgME::Key > keys = MessageComposer::Test::getKeys();
MessageComposer::Composer composer;
MessageComposer::EncryptJob *eJob = new MessageComposer::EncryptJob(&composer);
QVERIFY(eJob);
const QByteArray data(QString::fromLocal8Bit("one flew over the cuckoo's nest").toUtf8());
const QString subject(QStringLiteral("asdfghjklö"));
KMime::Content *content = new KMime::Content;
content->contentType(true)->setMimeType("text/plain");
content->setBody(data);
KMime::Message skeletonMessage;
skeletonMessage.contentType(true)->setMimeType("foo/bla");
skeletonMessage.to(true)->from7BitString("to@test.de, to2@test.de");
skeletonMessage.cc(true)->from7BitString("cc@test.de, cc2@test.de");
skeletonMessage.bcc(true)->from7BitString("bcc@test.de, bcc2@test.de");
skeletonMessage.subject(true)->fromUnicodeString(subject, "utf-8");
QStringList recipients;
recipients << QString::fromLocal8Bit("test@kolab.org");
eJob->setContent(content);
eJob->setCryptoMessageFormat(Kleo::OpenPGPMIMEFormat);
eJob->setRecipients(recipients);
eJob->setEncryptionKeys(keys);
eJob->setSkeletonMessage(&skeletonMessage);
eJob->setProtectedHeaders(protectedHeaders);
eJob->setProtectedHeadersObvoscate(protectedHeadersObvoscate);
VERIFYEXEC(eJob);
if (protectedHeadersObvoscate) {
QCOMPARE(skeletonMessage.subject()->as7BitString(false), "...");
} else {
QCOMPARE(skeletonMessage.subject()->asUnicodeString(), subject);
}
KMime::Content *result = eJob->content();
result->assemble();
KMime::Content *encPart = MessageComposer::Util::findTypeInMessage(result, "application", "octet-stream");
KMime::Content tempNode;
{
QByteArray plainText;
auto job = QGpgME::openpgp()->decryptVerifyJob();
job->exec(encPart->encodedBody(), plainText);
tempNode.setContent(KMime::CRLFtoLF(plainText.constData()));
tempNode.parse();
}
if (protectedHeadersObvoscate) {
tempNode.contentType(false)->setBoundary("123456789");
tempNode.assemble();
}
delete result;
QFile f(referenceFile);
QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate));
const QByteArray encodedContent(tempNode.encodedContent());
f.write(encodedContent);
if (!encodedContent.endsWith('\n')) {
f.write("\n");
}
f.close();
Test::compareFile(referenceFile, QStringLiteral(MAIL_DATA_DIR "/")+referenceFile);
}
void EncryptJobTest::checkEncryption(MessageComposer::EncryptJob *eJob)
{
VERIFYEXEC(eJob);
......
......@@ -40,8 +40,12 @@ public Q_SLOTS:
private Q_SLOTS:
void testContentDirect();
void testContentChained();
void testContentSubjobChained();
void testHeaders();
void testProtectedHeaders_data();
void testProtectedHeaders();
private:
void checkEncryption(MessageComposer::EncryptJob *eJob);
};
......
......@@ -24,9 +24,11 @@
#include <QGpgME/KeyListJob>
#include <gpgme++/keylistresult.h>
#include <QFile>
#include <QDir>
#include <QFile>
#include <QProcess>
#include <QStandardPaths>
#include <QTest>
using namespace MessageComposer;
......@@ -75,3 +77,34 @@ std::vector< GpgME::Key, std::allocator< GpgME::Key > > Test::getKeys(bool smime
return keys;
}
KMime::Message::Ptr Test::loadMessageFromFile(const QString &filename)
{
QFile file(QLatin1String(QByteArray(MAIL_DATA_DIR "/" + filename.toLatin1())));
const bool opened = file.open(QIODevice::ReadOnly);
Q_ASSERT(opened);
Q_UNUSED(opened);
const QByteArray data = KMime::CRLFtoLF(file.readAll());
Q_ASSERT(!data.isEmpty());
KMime::Message::Ptr msg(new KMime::Message);
msg->setContent(data);
msg->parse();
return msg;
}
void Test::compareFile(const QString &outFile, const QString &referenceFile)
{
QVERIFY(QFile::exists(outFile));
// compare to reference file
const auto args = QStringList()
<< QStringLiteral("-u")
<< referenceFile
<< outFile;
QProcess proc;
proc.setProcessChannelMode(QProcess::ForwardedChannels);
proc.start(QStringLiteral("diff"), args);
QVERIFY(proc.waitForFinished());
QCOMPARE(proc.exitCode(), 0);
}
......@@ -23,6 +23,8 @@
#include <gpgme++/key.h>
#include <KMime/Message>
namespace MessageComposer {
namespace Test {
/**
......@@ -37,6 +39,17 @@ void setupEnv();
* Returns list of keys used in various crypto routines
*/
std::vector<GpgME::Key> getKeys(bool smime = false);
/**
* Loads a message from filename and returns a message pointer
*/
KMime::Message::Ptr loadMessageFromFile(const QString &filename);
/**
* compare two mails via files.
* If the files are not euqal print diff output.
*/
void compareFile(const QString &outFile, const QString &referenceFile);
}
}
......
......@@ -35,6 +35,7 @@ set( messagecomposer_job_src
job/savecontactpreferencejob.cpp
job/attachmentvcardfromaddressbookjob.cpp
job/attachmentclipboardjob.cpp
job/protectedheaders.cpp
)
set( messagecomposer_statusbarwidget_src
......
......@@ -278,6 +278,7 @@ QList<ContentJobBase *> ComposerPrivate::createEncryptJobs(ContentJobBase *conte
eJob->setCryptoMessageFormat(format);
eJob->setEncryptionKeys(recipients.second);
eJob->setRecipients(recipients.first);
eJob->setSkeletonMessage(skeletonMessage);
subJob = eJob;
}
qCDebug(MESSAGECOMPOSER_LOG) << "subJob" << subJob;
......
......@@ -21,6 +21,7 @@
#include "job/encryptjob.h"
#include "contentjobbase_p.h"
#include "job/protectedheaders.h"
#include "utils/util_p.h"
#include <Libkleo/Enum>
......@@ -30,9 +31,6 @@
#include "messagecomposer_debug.h"
#include <kmime/kmime_message.h>
#include <kmime/kmime_content.h>
#include <gpgme++/global.h>
#include <gpgme++/signingresult.h>
#include <gpgme++/encryptionresult.h>
......@@ -52,6 +50,10 @@ public:
std::vector<GpgME::Key> keys;
Kleo::CryptoMessageFormat format;
KMime::Content *content = nullptr;
KMime::Message *skeletonMessage = nullptr;
bool protectedHeaders = true;
bool protectedHeadersObvoscate = false;
// copied from messagecomposer.cpp
bool binaryHint(Kleo::CryptoMessageFormat f)
......@@ -122,6 +124,27 @@ void EncryptJob::setRecipients(const QStringList &recipients)
d->recipients = recipients;
}
void EncryptJob::setSkeletonMessage(KMime::Message* skeletonMessage)
{
Q_D(EncryptJob);
d->skeletonMessage = skeletonMessage;
}
void EncryptJob::setProtectedHeaders(bool protectedHeaders)
{
Q_D(EncryptJob);
d->protectedHeaders = protectedHeaders;
}
void EncryptJob::setProtectedHeadersObvoscate(bool protectedHeadersObvoscate)
{
Q_D(EncryptJob);
d->protectedHeadersObvoscate = protectedHeadersObvoscate;
}
QStringList EncryptJob::recipients() const
{
Q_D(const EncryptJob);
......@@ -136,7 +159,7 @@ std::vector<GpgME::Key> EncryptJob::encryptionKeys() const
return d->keys;
}
void EncryptJob::process()
void EncryptJob::doStart()
{
Q_D(EncryptJob);
Q_ASSERT(d->resultContent == nullptr); // Not processed before.
......@@ -146,6 +169,54 @@ void EncryptJob::process()
return;
}
// if setContent hasn't been called, we assume that a subjob was added
// and we want to use that
if (!d->content || !d->content->hasContent()) {
if (d->subjobContents.size() == 1) {
d->content = d->subjobContents.first();
}
}
if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) {
ProtectedHeadersJob *pJob = new ProtectedHeadersJob;
pJob->setContent(d->content);
pJob->setSkeletonMessage(d->skeletonMessage);
pJob->setObvoscate(d->protectedHeadersObvoscate);
QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) {
if (job->error()) {
return;
}
d->content = pJob->content();
});
appendSubjob(pJob);
}
ContentJobBase::doStart();
}
void EncryptJob::slotResult(KJob *job)
{
Q_D(EncryptJob);
if (error()) {
ContentJobBase::slotResult(job);
return;
}
if (subjobs().size() == 2) {
auto pjob = static_cast<ProtectedHeadersJob *>(subjobs().last());
if (pjob) {
Q_ASSERT(dynamic_cast<ContentJobBase *>(job));
auto cjob = static_cast<ContentJobBase *>(job);
pjob->setContent(cjob->content());
}
}
ContentJobBase::slotResult(job);
}
void EncryptJob::process()
{
Q_D(EncryptJob);
// if setContent hasn't been called, we assume that a subjob was added
// and we want to use that
if (!d->content || !d->content->hasContent()) {
......@@ -153,8 +224,6 @@ void EncryptJob::process()
d->content = d->subjobContents.first();
}
//d->resultContent = new KMime::Content;
const QGpgME::Protocol *proto = nullptr;
if (d->format & Kleo::AnyOpenPGP) {
proto = QGpgME::openpgp();
......
......@@ -54,11 +54,17 @@ public:
void setCryptoMessageFormat(Kleo::CryptoMessageFormat format);
void setEncryptionKeys(const std::vector<GpgME::Key> &keys) override;
void setRecipients(const QStringList &rec) override;
void setSkeletonMessage(KMime::Message *skeletonMessage);
void setProtectedHeaders(bool protectedHeaders);
void setProtectedHeadersObvoscate(bool protectedHeadersObvoscate);
std::vector<GpgME::Key> encryptionKeys() const override;
QStringList recipients() const override;
protected Q_SLOTS:
void doStart() override;
void slotResult(KJob *job) override;
void process() override;
private:
......
/*
Copyright (C) 2020 Sandro Knauß <sknauss@kde.org>
This library is free software; you can redistribute it and/or modify it
under the terms of the GNU Library General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at your
option) any later version.
This library 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 Library General Public
License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301, USA.
*/
#include "job/protectedheaders.h"
#include "contentjobbase_p.h"
#include "job/singlepartjob.h"
#include "utils/util_p.h"
#include "messagecomposer_debug.h"
#include <kmime/kmime_message.h>
#include <kmime/kmime_content.h>
using namespace MessageComposer;
class MessageComposer::ProtectedHeadersJobPrivate : public ContentJobBasePrivate
{
public:
ProtectedHeadersJobPrivate(ProtectedHeadersJob *qq)
: ContentJobBasePrivate(qq)
{
}
KMime::Content *content = nullptr;
KMime::Message *skeletonMessage = nullptr;
bool obvoscate = false;
Q_DECLARE_PUBLIC(ProtectedHeadersJob)
};
ProtectedHeadersJob::ProtectedHeadersJob(QObject *parent)
: ContentJobBase(*new ProtectedHeadersJobPrivate(this), parent)
{
}
ProtectedHeadersJob::~ProtectedHeadersJob()
{
}
void ProtectedHeadersJob::setContent(KMime::Content *content)
{
Q_D(ProtectedHeadersJob);
d->content = content;
if (content)
{
d->content->assemble();
}
}
void ProtectedHeadersJob::setSkeletonMessage(KMime::Message *skeletonMessage)
{
Q_D(ProtectedHeadersJob);
d->skeletonMessage = skeletonMessage;
}
void ProtectedHeadersJob::setObvoscate(bool obvoscate)
{
Q_D(ProtectedHeadersJob);
d->obvoscate = obvoscate;
}
void ProtectedHeadersJob::doStart() {
Q_D(ProtectedHeadersJob);
Q_ASSERT(d->resultContent == nullptr); // Not processed before.
Q_ASSERT(d->skeletonMessage); // We need a skeletonMessage to proceed
auto subject = d->skeletonMessage->header<KMime::Headers::Subject>();
if (d->obvoscate && subject) {
// Create protected header lagacy mimepart with replaced headers
SinglepartJob *cjob = new SinglepartJob;
cjob->contentType()->setMimeType("text/plain");
cjob->contentType()->setCharset(subject->rfc2047Charset());
cjob->contentType()->setParameter(QStringLiteral("protected-headers"), QStringLiteral("v1"));
cjob->contentDisposition()->setDisposition(KMime::Headers::contentDisposition::CDinline);
cjob->setData(subject->type() + QByteArray(": ") + subject->asUnicodeString().toUtf8());
QObject::connect(cjob, &SinglepartJob::finished, this, [d, cjob](KJob *job) {
KMime::Content *mixedPart = new KMime::Content();
const QByteArray boundary = KMime::multiPartBoundary();
mixedPart->contentType()->setMimeType("multipart/mixed");
mixedPart->contentType()->setBoundary(boundary);
mixedPart->addContent(cjob->content());
// if setContent hasn't been called, we assume that a subjob was added
// and we want to use that
if (!d->content || !d->content->hasContent()) {
Q_ASSERT(d->subjobContents.size() == 1);
d->content = d->subjobContents.first();
}
mixedPart->addContent(d->content);
d->content = mixedPart;
});
appendSubjob(cjob);
}
ContentJobBase::doStart();
}
void ProtectedHeadersJob::process()
{
Q_D(ProtectedHeadersJob);
// if setContent hasn't been called, we assume that a subjob was added
// and we want to use that
if (!d->content || !d->content->hasContent()) {
Q_ASSERT(d->subjobContents.size() == 1);
d->content = d->subjobContents.first();
}
auto subject = d->skeletonMessage->header<KMime::Headers::Subject>();
const auto headers = d->skeletonMessage->headers();
for (const auto &header: headers) {
const QByteArray headerType(header->type());
if (headerType.startsWith("X-KMail-")) {
continue;
}
if (headerType == "MIME-Version") {
continue;
}
if (headerType == "Bcc") {
continue;
}
if (headerType.startsWith("Content-")) {
continue;
}
if (headerType == "Subject") {
KMime::Headers::Subject *copySubject = new KMime::Headers::Subject();