File indexing completed on 2024-11-10 04:50:03

0001 /*
0002  * SPDX-FileCopyrightText: 2017 Daniel Vrátil <dvratil@kde.org>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  *
0006  */
0007 
0008 #include "filteractionencrypt.h"
0009 #include "mailcommon_debug.h"
0010 #include "util/cryptoutils.h"
0011 
0012 #include <QCheckBox>
0013 #include <QEventLoop>
0014 #include <QLabel>
0015 #include <QVBoxLayout>
0016 
0017 #include <KLocalizedString>
0018 
0019 #include <KMime/Message>
0020 
0021 #include <QGpgME/EncryptJob>
0022 #include <QGpgME/KeyListJob>
0023 #include <QGpgME/ListAllKeysJob>
0024 #include <QGpgME/Protocol>
0025 #include <gpgme++/encryptionresult.h>
0026 #include <gpgme++/keylistresult.h>
0027 
0028 #include <Libkleo/DefaultKeyFilter>
0029 #include <Libkleo/KeySelectionCombo>
0030 
0031 #include <MessageComposer/EncryptJob>
0032 
0033 #include <Akonadi/MessageFlags>
0034 
0035 #include <KColorScheme>
0036 
0037 using namespace MailCommon;
0038 
0039 #define LISTING_FINISHED "listingFinished"
0040 #define IGNORE_KEY_CHANGE "ignoreKeyChange"
0041 
0042 FilterActionEncrypt::FilterActionEncrypt(QObject *parent)
0043     : FilterActionWithCrypto(QStringLiteral("encrypt"), i18n("Encrypt"), parent)
0044     , mKeyCache(Kleo::KeyCache::instance())
0045 {
0046 }
0047 
0048 FilterActionEncrypt::~FilterActionEncrypt() = default;
0049 
0050 FilterAction *FilterActionEncrypt::newAction()
0051 {
0052     return new FilterActionEncrypt();
0053 }
0054 
0055 QString FilterActionEncrypt::displayString() const
0056 {
0057     return label();
0058 }
0059 
0060 QString FilterActionEncrypt::argsAsString() const
0061 {
0062     if (mKey.isNull()) {
0063         return {};
0064     }
0065 
0066     const auto proto = ((mKey.protocol() == GpgME::OpenPGP) ? QStringLiteral("PGP") : QStringLiteral("SMIME"));
0067     return QStringLiteral("%1:%2:%3").arg(proto, QString::number(int(mReencrypt)), QString::fromLatin1(mKey.primaryFingerprint()));
0068 }
0069 
0070 void FilterActionEncrypt::argsFromString(const QString &argsStr)
0071 {
0072     const int pos = argsStr.indexOf(QLatin1Char(':'));
0073     const QStringView strView(argsStr);
0074     const auto protoStr = strView.left(pos);
0075 
0076     QGpgME::Protocol *proto = {};
0077     if (protoStr == QLatin1StringView("PGP")) {
0078         proto = QGpgME::openpgp();
0079     } else if (protoStr == QLatin1StringView("SMIME")) {
0080         proto = QGpgME::smime();
0081     } else {
0082         qCWarning(MAILCOMMON_LOG) << "Unknown protocol specified:" << protoStr;
0083         return;
0084     }
0085     mReencrypt = static_cast<bool>(QStringView(argsStr).mid(pos + 1, 1).toInt());
0086 
0087     const auto fp = argsStr.mid(pos + 3);
0088     auto listJob = proto->keyListJob(false, true, true);
0089 
0090     std::vector<GpgME::Key> keys;
0091     auto result = listJob->exec({fp}, true, keys);
0092     listJob->deleteLater();
0093 
0094     if (result.error()) {
0095         qCWarning(MAILCOMMON_LOG) << "Failed to retrieve keys:" << result.error().asString();
0096         return;
0097     }
0098 
0099     if (keys.empty()) {
0100         qCWarning(MAILCOMMON_LOG) << "Could not obtain configured key: key expired or removed?";
0101         // TODO: Interactively ask user to re-configure the filter
0102         return;
0103     }
0104 
0105     mKey = keys[0];
0106 }
0107 
0108 SearchRule::RequiredPart FilterActionEncrypt::requiredPart() const
0109 {
0110     return SearchRule::CompleteMessage;
0111 }
0112 
0113 FilterAction::ReturnCode FilterActionEncrypt::process(ItemContext &context, bool) const
0114 {
0115     if (mKey.isNull()) {
0116         qCWarning(MAILCOMMON_LOG) << "FilterActionEncrypt::process called without filter having a key!";
0117         return ErrorButGoOn;
0118     }
0119 
0120     auto &item = context.item();
0121     if (!item.hasPayload<KMime::Message::Ptr>()) {
0122         qCWarning(MAILCOMMON_LOG) << "Item" << item.id() << "does not contain KMime::Message payload!";
0123         return ErrorNeedComplete;
0124     }
0125 
0126     auto msg = item.payload<KMime::Message::Ptr>();
0127     if (KMime::isEncrypted(msg.data())) {
0128         if (mReencrypt) {
0129             // Make sure the email is not already encrypted by the mKey - this is
0130             // a little expensive, but still much cheaper than modifying and
0131             // re-uploading the email to the server
0132             const auto encryptionKeys = getEncryptionKeysFromContent(msg, mKey.protocol());
0133             qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "encrypted by following keys: " << encryptionKeys;
0134             if (!encryptionKeys.isEmpty()) {
0135                 if (mKey.protocol() == GpgME::OpenPGP) {
0136                     std::vector<std::string> ids;
0137                     ids.reserve(encryptionKeys.size());
0138                     for (const auto &key : encryptionKeys) {
0139                         ids.push_back(key.toStdString());
0140                     }
0141                     for (const auto &key : mKeyCache->findByKeyIDOrFingerprint(ids)) {
0142                         if (qstrcmp(key.primaryFingerprint(), mKey.primaryFingerprint()) == 0) {
0143                             // This email is already encrypted with the target key,
0144                             // so there's no need to re-encrypt it
0145                             qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting";
0146                             return GoOn;
0147                         }
0148                     }
0149                 } else if (mKey.protocol() == GpgME::CMS) {
0150                     // We are only able to get serial
0151                     for (const auto &key : mKeyCache->secretKeys()) {
0152                         if (qstrcmp(key.issuerSerial(), mKey.issuerSerial()) == 0) {
0153                             // This email is already encrypted with the target key,
0154                             // so there's no need to re-encrypt it
0155                             qCDebug(MAILCOMMON_LOG) << "Item" << item.id() << "already encrypted with" << mKey.primaryFingerprint() << ", not re-encrypting";
0156                             return GoOn;
0157                         }
0158                     }
0159                 }
0160             }
0161             bool dummy; // dummy
0162             const auto decrypted = CryptoUtils::decryptMessage(msg, dummy);
0163             if (!decrypted) {
0164                 // We failed to decrypt the encrypted email - very likely we just don't
0165                 // have the right key, so don't consider it an error
0166                 return GoOn;
0167             } else {
0168                 msg = decrypted;
0169             }
0170         } else {
0171             return GoOn;
0172         }
0173     }
0174 
0175     MessageComposer::EncryptJob encrypt;
0176     encrypt.setContent(msg.data());
0177     encrypt.setCryptoMessageFormat(mKey.protocol() == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat);
0178     encrypt.setEncryptionKeys({mKey});
0179     encrypt.exec();
0180     if (encrypt.error()) {
0181         qCWarning(MAILCOMMON_LOG) << "Encryption error:" << encrypt.errorString();
0182         return ErrorButGoOn;
0183     }
0184 
0185     KMime::Content *result = encrypt.content();
0186     result->assemble();
0187 
0188     auto nec = CryptoUtils::assembleMessage(msg, result);
0189     context.item().setPayload(nec);
0190     context.item().setFlag(Akonadi::MessageFlags::Encrypted);
0191     context.setNeedsPayloadStore();
0192     context.setNeedsFlagStore();
0193 
0194     delete result;
0195 
0196     return GoOn;
0197 }
0198 
0199 bool FilterActionEncrypt::isEmpty() const
0200 {
0201     return mKey.isNull();
0202 }
0203 
0204 QString FilterActionEncrypt::informationAboutNotValidAction() const
0205 {
0206     return i18n("No encryption key has been selected");
0207 }
0208 
0209 QWidget *FilterActionEncrypt::createParamWidget(QWidget *parent) const
0210 {
0211     auto w = new QWidget(parent);
0212     auto l = new QVBoxLayout;
0213     w->setLayout(l);
0214 
0215     auto combo = new Kleo::KeySelectionCombo(w);
0216     combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint()));
0217 
0218     std::shared_ptr<Kleo::DefaultKeyFilter> filter(new Kleo::DefaultKeyFilter);
0219     filter->setIsOpenPGP(Kleo::DefaultKeyFilter::DoesNotMatter);
0220     filter->setCanEncrypt(Kleo::DefaultKeyFilter::Set);
0221     filter->setHasSecret(Kleo::DefaultKeyFilter::Set);
0222     combo->setKeyFilter(filter);
0223 
0224     combo->setProperty(LISTING_FINISHED, false);
0225     combo->setProperty(IGNORE_KEY_CHANGE, false);
0226     connect(combo, &Kleo::KeySelectionCombo::keyListingFinished, combo, [combo] {
0227         combo->setProperty(LISTING_FINISHED, true);
0228         combo->setProperty(IGNORE_KEY_CHANGE, true);
0229     });
0230     connect(combo, &Kleo::KeySelectionCombo::currentKeyChanged, this, [this, combo]() {
0231         // Ignore key change due to the combo box populating itself after
0232         // finish
0233         if (!combo->property(IGNORE_KEY_CHANGE).toBool()) {
0234             Q_EMIT const_cast<FilterActionEncrypt *>(this)->filterActionModified();
0235         } else {
0236             combo->setProperty(IGNORE_KEY_CHANGE, false);
0237         }
0238     });
0239     l->addWidget(combo);
0240 
0241     auto chkBox = new QCheckBox(w);
0242     chkBox->setText(i18n("Re-encrypt encrypted emails with this key"));
0243     chkBox->setChecked(mReencrypt);
0244     connect(chkBox, &QCheckBox::toggled, this, &FilterActionEncrypt::filterActionModified);
0245     l->addWidget(chkBox);
0246 
0247     auto lbl = new QLabel(w);
0248     auto palette = lbl->palette();
0249     palette.setColor(lbl->foregroundRole(), KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText).color());
0250     lbl->setPalette(palette);
0251     lbl->setWordWrap(true);
0252     lbl->setText(i18n("<b>Warning:</b> Seckey necessary to read emails."));
0253     lbl->setToolTip(
0254         i18n("<p>Once an email has been encrypted you will need a crypto setup with "
0255              "your secret key to access the contents again.</p>"
0256              "<p>If you keep emails stored on an email server and use several clients, "
0257              "each of them must be configured to enable decryption.</p>"));
0258     l->addWidget(lbl);
0259 
0260     return w;
0261 }
0262 
0263 void FilterActionEncrypt::setParamWidgetValue(QWidget *paramWidget) const
0264 {
0265     if (auto combo = paramWidget->findChild<Kleo::KeySelectionCombo *>()) {
0266         combo->setDefaultKey(QString::fromLatin1(mKey.primaryFingerprint()));
0267         combo->setCurrentKey(QString::fromLatin1(mKey.primaryFingerprint()));
0268     }
0269     if (auto chkBox = paramWidget->findChild<QCheckBox *>()) {
0270         chkBox->setChecked(mReencrypt);
0271     }
0272 }
0273 
0274 void FilterActionEncrypt::applyParamWidgetValue(QWidget *paramWidget)
0275 {
0276     if (auto combo = paramWidget->findChild<Kleo::KeySelectionCombo *>()) {
0277         // FIXME: This is super-ugly, but unfortunately the filtering code generates
0278         // several instances of this filter and passes the paramWidgets from one
0279         // instance to another to "copy" stuff in between, which in our case leads
0280         // to this method being called on an un-populated combobox
0281         if (!combo->property(LISTING_FINISHED).toBool()) {
0282             QEventLoop ev;
0283             connect(combo, &Kleo::KeySelectionCombo::keyListingFinished, &ev, &QEventLoop::quit, Qt::QueuedConnection);
0284             ev.exec();
0285         }
0286         mKey = combo->currentKey();
0287     }
0288     if (auto chkBox = paramWidget->findChild<QCheckBox *>()) {
0289         mReencrypt = chkBox->isChecked();
0290     }
0291 }
0292 
0293 GpgME::Key FilterActionEncrypt::key() const
0294 {
0295     return mKey;
0296 }
0297 
0298 bool FilterActionEncrypt::reencrypt() const
0299 {
0300     return mReencrypt;
0301 }
0302 
0303 #include "moc_filteractionencrypt.cpp"