File indexing completed on 2024-06-23 05:18:31

0001 /*
0002   SPDX-FileCopyrightText: 2020 Sandro Knauß <sknauss@kde.org>
0003 
0004   SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "job/autocryptheadersjob.h"
0008 
0009 #include "contentjobbase_p.h"
0010 
0011 #include "job/singlepartjob.h"
0012 #include "utils/util_p.h"
0013 
0014 #include "messagecomposer_debug.h"
0015 
0016 #include <QGpgME/ExportJob>
0017 #include <QGpgME/Protocol>
0018 #include <gpgme++/context.h>
0019 
0020 #include <KCodecs>
0021 #include <KLocalizedString>
0022 #include <KMime/Content>
0023 #include <KMime/Headers>
0024 
0025 #include <QByteArray>
0026 
0027 #include <map>
0028 
0029 using namespace MessageComposer;
0030 
0031 class MessageComposer::AutocryptHeadersJobPrivate : public ContentJobBasePrivate
0032 {
0033 public:
0034     AutocryptHeadersJobPrivate(AutocryptHeadersJob *qq)
0035         : ContentJobBasePrivate(qq)
0036     {
0037     }
0038 
0039     ~AutocryptHeadersJobPrivate() override
0040     {
0041         // clean up in case of cancelled job
0042         for (const auto &[key, header] : gossipHeaders) {
0043             delete header;
0044         }
0045         gossipHeaders.clear();
0046     }
0047 
0048     void emitGpgError(const GpgME::Error &error);
0049     void emitNotFoundError(const QByteArray &addr, const QByteArray &fingerprint);
0050     void fillHeaderData(KMime::Headers::Generic *header, const QByteArray &addr, bool preferEncrypted, const QByteArray &keydata);
0051     void finishOnLastSubJob();
0052 
0053     KMime::Content *content = nullptr;
0054     KMime::Message *skeletonMessage = nullptr;
0055     // used to ensure consistent order based on key order, not random one by async subjobs delivering
0056     std::map<QByteArray, KMime::Headers::Generic *> gossipHeaders;
0057 
0058     bool preferEncrypted = false;
0059     int subJobs = 0;
0060 
0061     QString gnupgHome;
0062     GpgME::Key recipientKey;
0063     std::vector<GpgME::Key> gossipKeys;
0064 
0065     Q_DECLARE_PUBLIC(AutocryptHeadersJob)
0066 };
0067 
0068 void AutocryptHeadersJobPrivate::finishOnLastSubJob()
0069 {
0070     Q_Q(AutocryptHeadersJob);
0071 
0072     if (subJobs > 0) {
0073         return;
0074     }
0075 
0076     for (const auto &[key, header] : gossipHeaders) {
0077         content->appendHeader(header);
0078     }
0079     gossipHeaders.clear();
0080     resultContent = content;
0081 
0082     q->emitResult();
0083 }
0084 
0085 void AutocryptHeadersJobPrivate::emitGpgError(const GpgME::Error &error)
0086 {
0087     Q_Q(AutocryptHeadersJob);
0088 
0089     Q_ASSERT(error);
0090     const QString msg = i18n(
0091         "<p>An error occurred while trying to export "
0092         "the key from the backend:</p>"
0093         "<p><b>%1</b></p>",
0094         QString::fromLocal8Bit(error.asString()));
0095     q->setError(KJob::UserDefinedError);
0096     q->setErrorText(msg);
0097     q->emitResult();
0098 }
0099 
0100 void AutocryptHeadersJobPrivate::emitNotFoundError(const QByteArray &addr, const QByteArray &fingerprint)
0101 {
0102     Q_Q(AutocryptHeadersJob);
0103     const QString msg = i18n(
0104         "<p>An error occurred while trying to export "
0105         "the key from the backend:</p>"
0106         "<p><b>No valid key found for user %1 (%2)</b></p>",
0107         QString::fromLatin1(addr),
0108         QString::fromLatin1(fingerprint));
0109     q->setError(KJob::UserDefinedError);
0110     q->setErrorText(msg);
0111     q->emitResult();
0112 }
0113 
0114 void AutocryptHeadersJobPrivate::fillHeaderData(KMime::Headers::Generic *header, const QByteArray &addr, bool preferEncrypted, const QByteArray &keydata)
0115 {
0116     QByteArray parameters = "addr=" + addr + "; ";
0117     if (preferEncrypted) {
0118         parameters += "prefer-encrypt=mutual; ";
0119     }
0120     parameters += "keydata=\n ";
0121     auto encoded = KCodecs::base64Encode(keydata).replace('\n', QByteArray());
0122     const auto length = encoded.size();
0123     const auto lineLength = 76;
0124     auto start = 0;
0125     auto column = 1;
0126     while (start < length) {
0127         const auto midLength = std::min<int>(length - start, lineLength - column);
0128         parameters += encoded.mid(start, midLength);
0129         start += midLength;
0130         column += midLength;
0131         if (column >= lineLength) {
0132             parameters += "\n ";
0133             column = 1;
0134         }
0135     }
0136     header->from7BitString(parameters);
0137 }
0138 
0139 AutocryptHeadersJob::AutocryptHeadersJob(QObject *parent)
0140     : ContentJobBase(*new AutocryptHeadersJobPrivate(this), parent)
0141 {
0142 }
0143 
0144 AutocryptHeadersJob::~AutocryptHeadersJob() = default;
0145 
0146 void AutocryptHeadersJob::setContent(KMime::Content *content)
0147 {
0148     Q_D(AutocryptHeadersJob);
0149 
0150     d->content = content;
0151     if (content) {
0152         d->content->assemble();
0153     }
0154 }
0155 
0156 void AutocryptHeadersJob::setSkeletonMessage(KMime::Message *skeletonMessage)
0157 {
0158     Q_D(AutocryptHeadersJob);
0159 
0160     d->skeletonMessage = skeletonMessage;
0161 }
0162 
0163 void AutocryptHeadersJob::setGnupgHome(const QString &path)
0164 {
0165     Q_D(AutocryptHeadersJob);
0166 
0167     d->gnupgHome = path;
0168 }
0169 
0170 void AutocryptHeadersJob::setSenderKey(const GpgME::Key &key)
0171 {
0172     Q_D(AutocryptHeadersJob);
0173 
0174     d->recipientKey = key;
0175 }
0176 
0177 void AutocryptHeadersJob::setPreferEncrypted(bool preferEncrypted)
0178 {
0179     Q_D(AutocryptHeadersJob);
0180 
0181     d->preferEncrypted = preferEncrypted;
0182 }
0183 
0184 void AutocryptHeadersJob::setGossipKeys(const std::vector<GpgME::Key> &gossipKeys)
0185 {
0186     Q_D(AutocryptHeadersJob);
0187 
0188     d->gossipKeys = gossipKeys;
0189 }
0190 
0191 void AutocryptHeadersJob::process()
0192 {
0193     Q_D(AutocryptHeadersJob);
0194     Q_ASSERT(d->resultContent == nullptr); // Not processed before.
0195 
0196     // if setContent hasn't been called, we assume that a subjob was added
0197     // and we want to use that
0198     if (!d->content) {
0199         Q_ASSERT(d->subjobContents.size() == 1);
0200         d->content = d->subjobContents.constFirst();
0201     }
0202 
0203     auto job = QGpgME::openpgp()->publicKeyExportJob(false);
0204     Q_ASSERT(job);
0205 
0206     if (!d->gnupgHome.isEmpty()) {
0207         QGpgME::Job::context(job)->setEngineHomeDirectory(d->gnupgHome.toUtf8().constData());
0208     }
0209     if (!d->recipientKey.isNull() && !d->recipientKey.isInvalid()) {
0210         connect(job, &QGpgME::ExportJob::result, this, [this, d](const GpgME::Error &error, const QByteArray &keydata) {
0211             d->subJobs--;
0212             if (AutocryptHeadersJob::error()) {
0213                 // When the job already has failed do nothing.
0214                 return;
0215             }
0216             if (error) {
0217                 d->emitGpgError(error);
0218                 return;
0219             }
0220             if (keydata.isEmpty()) {
0221                 d->emitNotFoundError(d->skeletonMessage->from()->addresses()[0], d->recipientKey.primaryFingerprint());
0222                 return;
0223             }
0224 
0225             auto autocrypt = new KMime::Headers::Generic("Autocrypt");
0226             d->fillHeaderData(autocrypt, d->skeletonMessage->from()->addresses()[0], d->preferEncrypted, keydata);
0227 
0228             d->skeletonMessage->setHeader(autocrypt);
0229             d->skeletonMessage->assemble();
0230 
0231             d->finishOnLastSubJob();
0232         });
0233         d->subJobs++;
0234         job->start(QStringList(QString::fromLatin1(d->recipientKey.primaryFingerprint())));
0235         job->setExportFlags(GpgME::Context::ExportMinimal);
0236     }
0237 
0238     const auto keys = d->gossipKeys;
0239     for (const auto &key : keys) {
0240         if (QByteArray(key.primaryFingerprint()) == QByteArray(d->recipientKey.primaryFingerprint())) {
0241             continue;
0242         }
0243 
0244         auto gossipJob = QGpgME::openpgp()->publicKeyExportJob(false);
0245         Q_ASSERT(gossipJob);
0246 
0247         if (!d->gnupgHome.isEmpty()) {
0248             QGpgME::Job::context(gossipJob)->setEngineHomeDirectory(d->gnupgHome.toUtf8().constData());
0249         }
0250 
0251         connect(gossipJob, &QGpgME::ExportJob::result, this, [this, d, key](const GpgME::Error &error, const QByteArray &keydata) {
0252             d->subJobs--;
0253             if (AutocryptHeadersJob::error()) {
0254                 // When the job already has failed do nothing.
0255                 return;
0256             }
0257             if (error) {
0258                 d->emitGpgError(error);
0259                 return;
0260             }
0261             if (keydata.isEmpty()) {
0262                 d->emitNotFoundError(key.userID(0).email(), key.primaryFingerprint());
0263                 return;
0264             }
0265 
0266             auto header = new KMime::Headers::Generic("Autocrypt-Gossip");
0267             d->fillHeaderData(header, key.userID(0).email(), false, keydata);
0268 
0269             d->gossipHeaders.insert({QByteArray(key.primaryFingerprint()), header});
0270 
0271             d->finishOnLastSubJob();
0272         });
0273 
0274         d->subJobs++;
0275         gossipJob->start(QStringList(QString::fromLatin1(key.primaryFingerprint())));
0276         gossipJob->setExportFlags(GpgME::Context::ExportMinimal);
0277     }
0278     if (d->subJobs == 0) {
0279         d->resultContent = d->content;
0280         emitResult();
0281     }
0282 }
0283 
0284 #include "moc_autocryptheadersjob.cpp"