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

messagecore/autocrypt: Add AtuocryptUtils.

parent 4e7c87c5
......@@ -56,3 +56,18 @@ target_link_libraries(autocryptstoragetest
)
ecm_mark_as_test(messagecore-autocryptstoragetest)
add_gpg_crypto_test(autocryptstoragetest messagecore-autocryptstoragetest)
add_executable(autocryptutilstest
autocryptutilstest.cpp
../src/autocrypt/autocryptrecipient.cpp
)
target_link_libraries(autocryptutilstest
Qt5::Test
KF5::Libkleo KF5::MessageCore Qt5::Widgets
KF5::Archive KF5::Completion KF5::ConfigCore
KF5::CoreAddons KF5::Codecs KF5::MimeTreeParser
QGpgme Gpgmepp
)
ecm_mark_as_test(messagecore-autocryptutilstest)
add_gpg_crypto_test(autocryptutilstest messagecore-autocryptutilstest)
/* SPDX-FileCopyrightText: 2020 Sandro Knauß <sknauss@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "autocryptutilstest.h"
#include "autocrypt/autocryptutils.h"
#include "autocrypt/autocryptstorage.h"
#include "autocrypt/autocryptstorage_p.h"
#include "autocrypt/autocryptrecipient_p.h"
#include <MimeTreeParser/NodeHelper>
#include <MimeTreeParser/SimpleObjectTreeSource>
#include <MimeTreeParser/ObjectTreeParser>
#include <QFile>
#include <QStandardPaths>
#include <QTest>
#include <QVector>
using namespace MessageCore;
QTEST_MAIN(AutocryptUtilsTest)
KMime::Message::Ptr readAndParseMail(const QString &mailFile)
{
QFile file(QLatin1String(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile);
file.open(QIODevice::ReadOnly);
Q_ASSERT(file.isOpen());
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 AutocryptUtilsTest::initTestCase()
{
qputenv("LC_ALL", "C");
QStandardPaths::setTestModeEnabled(true);
qputenv("KDEHOME", QFile::encodeName(QDir::homePath() + QLatin1String("/.qttest")).constData());
const QDir genericDataLocation(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation));
baseDir = QDir(genericDataLocation.filePath(QStringLiteral("autocrypt")));
}
void AutocryptUtilsTest::init()
{
baseDir.removeRecursively();
baseDir.mkpath(QStringLiteral("."));
}
void AutocryptUtilsTest::cleanup()
{
baseDir.removeRecursively();
auto storage = AutocryptStorage::self();
storage->d_func()->recipients.clear();
}
void AutocryptUtilsTest::test_header()
{
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/header.mbox"));
HeaderMixupNodeHelper gossipMixin(&nodeHelper, message.data());
MimeTreeParser::SimpleObjectTreeSource testSource;
MimeTreeParser::ObjectTreeParser otp(&testSource, &nodeHelper);
testSource.setDecryptMessage(true);
otp.parseObjectTree(message.data());
processAutocryptfromMail(gossipMixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.keys().size(), 1);
const QByteArray addr("alice@autocrypt.example");
QVERIFY2(storage->getRecipient(addr),
qPrintable(QStringLiteral("storage missing %1").arg(QString::fromLatin1(addr))));
QCOMPARE(storage->getRecipient(addr)->gpgKey().isNull(), false);
QCOMPARE(storage->getRecipient(addr)->gossipKey().isNull(), true);
QFile data(QLatin1String(DATA_DIR) + QStringLiteral("/autocrypt/alice.json"));
QVERIFY(data.open(QIODevice::ReadOnly | QIODevice::Text));
QCOMPARE(storage->getRecipient(addr)->toJson(QJsonDocument::Compact), data.readAll().trimmed());
data.close();
}
void AutocryptUtilsTest::test_gossip()
{
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/gossipheader.mbox"));
HeaderMixupNodeHelper gossipMixin(&nodeHelper, message.data());
MimeTreeParser::SimpleObjectTreeSource testSource;
MimeTreeParser::ObjectTreeParser otp(&testSource, &nodeHelper);
testSource.setDecryptMessage(true);
otp.parseObjectTree(message.data());
processAutocryptfromMail(gossipMixin);
QSet<QByteArray> expectedKeys;
expectedKeys << "alice@autocrypt.example"
<< "bob@autocrypt.example"
<< "carol@autocrypt.example";
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.keys().size(), expectedKeys.size());
for(const auto addr: expectedKeys) {
QVERIFY2(storage->getRecipient(addr),
qPrintable(QStringLiteral("storage missing %1").arg(QString::fromLatin1(addr))));
}
QCOMPARE(storage->getRecipient("alice@autocrypt.example")->gossipKey().isNull(), true);
QCOMPARE(storage->getRecipient("bob@autocrypt.example")->gpgKey().isNull(), true);
QCOMPARE(storage->getRecipient("bob@autocrypt.example")->gossipKey().isNull(), false);
QCOMPARE(storage->getRecipient("carol@autocrypt.example")->gpgKey().isNull(), true);
QCOMPARE(storage->getRecipient("carol@autocrypt.example")->gossipKey().isNull(), false);
}
void AutocryptUtilsTest::test_gossipIgnoreNonExcrypted()
{
// Only parse Autocrypt-Gossip headers in the encrypted payload.
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/gossipheader-nonencrypted.mbox"));
HeaderMixupNodeHelper gossipMixin(&nodeHelper, message.data());
MimeTreeParser::SimpleObjectTreeSource testSource;
MimeTreeParser::ObjectTreeParser otp(&testSource, &nodeHelper);
testSource.setDecryptMessage(true);
otp.parseObjectTree(message.data());
processAutocryptfromMail(gossipMixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.isEmpty(), true);
}
void AutocryptUtilsTest::test_draft()
{
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/draft.mbox"));
HeaderMixupNodeHelper gossipMixin(&nodeHelper, message.data());
MimeTreeParser::SimpleObjectTreeSource testSource;
MimeTreeParser::ObjectTreeParser otp(&testSource, &nodeHelper);
testSource.setDecryptMessage(true);
otp.parseObjectTree(message.data());
processAutocryptfromMail(gossipMixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.size(), 1);
}
void AutocryptUtilsTest::test_report()
{
// If content-type is multipart/report ignore message.
MimeTreeParser::NodeHelper nodeHelper;
{
auto message = readAndParseMail(QStringLiteral("autocrypt/header.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
processAutocryptfromMail(mixin);
}
auto storage = AutocryptStorage::self();
const auto alice = storage->getRecipient("alice@autocrypt.example");
QVERIFY(alice);
QCOMPARE(alice->count_have_ach(), 1);
QCOMPARE(alice->count_no_ach(), 0);
{
auto message = readAndParseMail(QStringLiteral("autocrypt/mdn.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
processAutocryptfromMail(mixin);
}
QCOMPARE(alice->count_have_ach(), 1);
QCOMPARE(alice->count_no_ach(), 0);
}
void AutocryptUtilsTest::test_multiple_headers()
{
// If there is more than one valid header, this SHOULD be treated as an
// error, and all Autocrypt headers discarded as invalid.
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/multiple_autocryptheaders.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
processAutocryptfromMail(mixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.size(), 0);
}
void AutocryptUtilsTest::test_multiple_headers_invalid()
{
// If there multiple autocrypt headers but only one is valid use this.
//
// An attribute name without a leading underscore is a “critical”
// attribute.It MUST treat the entire Autocrypt header as invalid if it
// encounters a “critical” attribute that it doesn’t support.
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/multiple_autocryptheaders_invalid.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
processAutocryptfromMail(mixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.size(), 1);
}
void AutocryptUtilsTest::test_multiple_headers_invalid_from()
{
// If there multiple autocrypt headers but only one is valid use this.
//
// The addr attribute is mandatory, and contains the single recipient
// address this header is valid for. If this address differs from the one
// in the From header, the entire Autocrypt header MUST be treated as
// invalid.
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/multiple_autocryptheaders_invalid_from.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
processAutocryptfromMail(mixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.size(), 1);
}
void AutocryptUtilsTest::test_multiple_from()
{
// Messages SHOULD be ignored, if there is more than one address in the
// From header.
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/multiple_from.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
processAutocryptfromMail(mixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.size(), 0);
}
void AutocryptUtilsTest::test_non_autocrypt()
{
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("html.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
processAutocryptfromMail(mixin);
auto storage = AutocryptStorage::self();
QCOMPARE(storage->d_func()->recipients.size(), 0);
}
void AutocryptUtilsTest::test_update_autocrypt()
{
MimeTreeParser::NodeHelper nodeHelper;
auto message = readAndParseMail(QStringLiteral("autocrypt/header.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
auto messageDate = QDateTime::currentDateTime().addYears(-1);
message->date()->setDateTime(messageDate);
processAutocryptfromMail(mixin);
auto newDate = messageDate.addDays(2);
message->date()->setDateTime(newDate);
processAutocryptfromMail(mixin);
auto storage = AutocryptStorage::self();
const auto alice = storage->getRecipient("alice@autocrypt.example");
QVERIFY(alice);
QCOMPARE(alice->count_have_ach(), 2);
QCOMPARE(alice->autocrypt_timestamp(), newDate);
QCOMPARE(alice->count_no_ach(), 0);
}
void AutocryptUtilsTest::test_update_non_autocrypt()
{
MimeTreeParser::NodeHelper nodeHelper;
auto messageDate = QDateTime::currentDateTime().addYears(-1);
auto newDate = messageDate.addDays(2);
{
auto message = readAndParseMail(QStringLiteral("autocrypt/header.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
message->date()->setDateTime(messageDate);
processAutocryptfromMail(mixin);
}
{
auto message = readAndParseMail(QStringLiteral("html.mbox"));
HeaderMixupNodeHelper noAutocrypt(&nodeHelper, message.data());
message->from()->from7BitString("alice@autocrypt.example");
message->date()->setDateTime(newDate);
processAutocryptfromMail(noAutocrypt);
}
auto storage = AutocryptStorage::self();
const auto alice = storage->getRecipient("alice@autocrypt.example");
QVERIFY(alice);
QCOMPARE(alice->count_have_ach(), 1);
QCOMPARE(alice->autocrypt_timestamp(), newDate);
QCOMPARE(alice->count_no_ach(), 1);
}
void AutocryptUtilsTest::test_update_autocrypt_gossip()
{
MimeTreeParser::NodeHelper nodeHelper;
auto messageDate = QDateTime::currentDateTime().addYears(-1);
auto newDate = messageDate.addDays(2);
{
auto message = readAndParseMail(QStringLiteral("autocrypt/header.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
message->date()->setDateTime(messageDate);
processAutocryptfromMail(mixin);
}
{
auto message = readAndParseMail(QStringLiteral("autocrypt/alice_gossip.mbox"));
HeaderMixupNodeHelper mixin(&nodeHelper, message.data());
message->date()->setDateTime(newDate);
MimeTreeParser::SimpleObjectTreeSource testSource;
MimeTreeParser::ObjectTreeParser otp(&testSource, &nodeHelper);
testSource.setDecryptMessage(true);
otp.parseObjectTree(message.data());
auto extraContent = nodeHelper.decryptedNodeForContent(message.data());
QVERIFY(extraContent);
auto dateHeader = static_cast<KMime::Headers::Date *>(extraContent->headerByType("date"));
dateHeader->setDateTime(newDate);
processAutocryptfromMail(mixin);
}
auto storage = AutocryptStorage::self();
const auto alice = storage->getRecipient("alice@autocrypt.example");
QVERIFY(alice);
QCOMPARE(alice->count_have_ach(), 1);
QCOMPARE(alice->autocrypt_timestamp(), messageDate);
QCOMPARE(alice->count_no_ach(), 0);
QCOMPARE(alice->gossip_timestamp(), newDate);
}
/* SPDX-FileCopyrightText: 2020 Sandro Knauß <sknauss@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef AUTOCRYPTUTILSTEST_H
#define AUTOCRYPTUTILSTEST_H
#include <QObject>
#include <QDir>
class AutocryptUtilsTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void init();
void cleanup();
void test_header();
void test_gossip();
void test_gossipIgnoreNonExcrypted();
void test_draft();
void test_report();
void test_multiple_headers();
void test_multiple_headers_invalid();
void test_multiple_headers_invalid_from();
void test_multiple_from();
void test_non_autocrypt();
void test_update_autocrypt();
void test_update_non_autocrypt();
void test_update_autocrypt_gossip();
private:
QDir baseDir;
};
#endif
......@@ -2,6 +2,7 @@ add_definitions(-DTRANSLATION_DOMAIN=\"libmessagecore\")
########### next target ###############
set(messagecore_autocrypt_LIB_SRCS
autocrypt/autocryptutils.cpp
autocrypt/autocryptrecipient.cpp
autocrypt/autocryptstorage.cpp
)
......@@ -59,6 +60,10 @@ ecm_qt_declare_logging_category(messagecore_LIB_SRCS HEADER messagecore_debug.h
EXPORT MESSAGELIB
)
ecm_qt_declare_logging_category(messagecore_LIB_SRCS HEADER autocrypt_debug.h IDENTIFIER AUTOCRYPT_LOG CATEGORY_NAME org.kde.pim.messagecore.autocrypt
DESCRIPTION "messagelib (messagecore.autocrypt)"
EXPORT MESSAGELIB
)
add_library(KF5MessageCore ${messagecore_LIB_SRCS})
if (COMPILE_WITH_UNITY_CMAKE_SUPPORT)
......
......@@ -7,6 +7,8 @@
#include "autocryptrecipient.h"
#include "autocryptrecipient_p.h"
#include "autocryptutils.h"
#include<KMime/Headers>
#include <QJsonDocument>
......@@ -156,17 +158,13 @@ void AutocryptRecipient::updateFromMessage(const HeaderMixupNodeHelper& mixup, c
}
if (header) {
const auto &parts = header.split(';');
QHash<QByteArray,QByteArray> params;
for(const auto &part: parts) {
const auto &i = part.split('=');
params[i[0].trimmed()] = i[1].trimmed();
}
const auto params = paramsFromAutocryptHeader(header);
if (d->addr.isEmpty()) {
d->addr = params["addr"];
d->addr = params.value("addr");
}
d->prefer_encrypt = params.contains("prefer-encrypt");
d->keydata = params["keydata"].replace(' ', QByteArray());
d->keydata = params.value("keydata");
d->keydata.replace(' ', QByteArray());
d->last_seen = effectiveDate;
d->count_have_ach += 1;
......@@ -191,22 +189,18 @@ void AutocryptRecipient::updateFromGossip(const HeaderMixupNodeHelper& mixup, co
return;
}
const auto &parts = header->as7BitString(false).split(';');
QHash<QByteArray,QByteArray> params;
for(const auto &part: parts) {
const auto &i = part.split('=');
params[i[0].trimmed()] = i[1].trimmed();
}
const auto params = paramsFromAutocryptHeader(header);
if (d->addr.isEmpty()) {
d->addr = params["addr"];
} else if (d->addr != params["addr"]) {
d->addr = params.value("addr");
} else if (d->addr != params.value("addr")) {
return;
}
d->changed = true;
d->gossip_timestamp = effectiveDate;
d->gossip_key = params["keydata"].replace(' ', QByteArray());
d->gossip_key = params.value("keydata");
d->gossip_key.replace(' ', QByteArray());
}
QByteArray AutocryptRecipient::toJson (QJsonDocument::JsonFormat format) const
......
......@@ -10,6 +10,7 @@
#include "autocryptrecipient.h"
class AutocryptStorageTest;
class AutocryptUtilsTest;
namespace MessageCore {
......@@ -35,6 +36,7 @@ private:
Q_DECLARE_PRIVATE(AutocryptStorage)
friend class ::AutocryptStorageTest;
friend class ::AutocryptUtilsTest;
};
}
......
/*
SPDX-FileCopyrightText: 2020 Sandro Kanuß <sknauss@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "autocryptutils.h"
#include "autocryptstorage.h"
#include "autocrypt_debug.h"
#include <MimeTreeParser/MessagePart>
#include <KMime/Headers>
#include <QHash>
using namespace MessageCore;
QHash<QByteArray,QByteArray> MessageCore::paramsFromAutocryptHeader(const KMime::Headers::Base *const header)
{
QHash<QByteArray,QByteArray> params;
const auto &parts = header->as7BitString(false).split(';');
for(const auto &part: parts) {
const auto &i = part.split('=');
params[i[0].trimmed()] = i[1].trimmed();
}
return params;
}
void MessageCore::processAutocryptfromMail(const HeaderMixupNodeHelper& mixup)
{
if (mixup.hasMailHeader("Autocrypt-Draft-State")) {
qInfo(AUTOCRYPT_LOG) << "Don't update Autocrypt storage from draft" << mixup.mailHeaderAsBase("Message-ID")->as7BitString(false);
return;
}
auto storage = AutocryptStorage::self();
const auto fromAddr = mixup.mailHeaderAsAddressList("from");
if (fromAddr->addresses().size() != 1) {
qInfo(AUTOCRYPT_LOG) << "Don't update Autocrypt storage, because we have multiple From addresses found in " << mixup.mailHeaderAsBase("Message-ID")->as7BitString(false);
return;
}
if (mixup.hasMailHeader("Autocrypt")) {
KMime::Headers::Base *header=nullptr;
int valid = 0;
for(const auto h: mixup.headers("Autocrypt")) {
const auto params = paramsFromAutocryptHeader(h);
if (params.value("addr") != fromAddr->addresses().value(0)) {
continue;
}
bool invalid = false;
for(const auto key: params.keys()) {
if (key == "addr") {
continue;
} else if (key == "prefer-encrypt") {
continue;
} else if (key == "keydata") {
continue;
} else if (key[0] == '_') {
continue;
} else {
invalid = true;
break;
}
}
if (invalid) {
continue;
}
if (!header) {
header = h;
}
valid++;
}
if (valid == 1) {
const auto params = paramsFromAutocryptHeader(header);
auto recipient = storage->addRecipient(params.value("addr"));
recipient->updateFromMessage(mixup, header);
qInfo(AUTOCRYPT_LOG) << "Update Autocrypt information for " << recipient->addr() << " from " << mixup.mailHeaderAsBase("Message-ID")->as7BitString(false);
} else {
qInfo(AUTOCRYPT_LOG) << "Don't update Autocrypt storage, because we have multiple valid Autocrypt headers found in " << mixup.mailHeaderAsBase("Message-ID")->as7BitString(false);
}
} else {
auto recipient = storage->getRecipient(fromAddr->addresses().value(0));
if (recipient) {
recipient->updateFromMessage(mixup, nullptr);
qInfo(AUTOCRYPT_LOG) << "Update Autocrypt information for " << recipient->addr() << " from " << mixup.mailHeaderAsBase("Message-ID")->as7BitString(false);