File indexing completed on 2024-06-23 05:13:42

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     commands/importcertificatescommand.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2007, 2008 Klarälvdalens Datakonsult AB
0006     SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik
0007     SPDX-FileContributor: Intevation GmbH
0008     SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH
0009     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0010 
0011     SPDX-License-Identifier: GPL-2.0-or-later
0012 */
0013 
0014 #include <config-kleopatra.h>
0015 
0016 #include "importcertificatescommand.h"
0017 #include "importcertificatescommand_p.h"
0018 
0019 #include "certifycertificatecommand.h"
0020 #include "kleopatra_debug.h"
0021 #include <settings.h>
0022 #include <utils/memory-helpers.h>
0023 
0024 #include <Libkleo/Algorithm>
0025 #include <Libkleo/Formatting>
0026 #include <Libkleo/KeyCache>
0027 #include <Libkleo/KeyGroupImportExport>
0028 #include <Libkleo/KeyHelpers>
0029 #include <Libkleo/KeyList>
0030 #include <Libkleo/KeyListSortFilterProxyModel>
0031 #include <Libkleo/MessageBox>
0032 #include <Libkleo/Predicates>
0033 #include <Libkleo/Stl_Util>
0034 
0035 #include <QGpgME/ChangeOwnerTrustJob>
0036 #include <QGpgME/ImportFromKeyserverJob>
0037 #include <QGpgME/ImportJob>
0038 #include <QGpgME/Protocol>
0039 #include <QGpgME/ReceiveKeysJob>
0040 
0041 #include <gpgme++/context.h>
0042 #include <gpgme++/global.h>
0043 #include <gpgme++/importresult.h>
0044 #include <gpgme++/key.h>
0045 #include <gpgme++/keylistresult.h>
0046 
0047 #include <KLocalizedString>
0048 #include <KMessageBox>
0049 
0050 #include <QByteArray>
0051 #include <QEventLoop>
0052 #include <QProgressDialog>
0053 #include <QString>
0054 #include <QTextDocument> // for Qt::escape
0055 #include <QTreeView>
0056 #include <QWidget>
0057 
0058 #include <algorithm>
0059 #include <map>
0060 #include <memory>
0061 #include <set>
0062 #include <unordered_set>
0063 
0064 using namespace GpgME;
0065 using namespace Kleo;
0066 using namespace QGpgME;
0067 
0068 static void disconnectConnection(const QMetaObject::Connection &connection)
0069 {
0070     // trivial function for disconnecting a signal-slot connection
0071     QObject::disconnect(connection);
0072 }
0073 
0074 bool operator==(const ImportJobData &lhs, const ImportJobData &rhs)
0075 {
0076     return lhs.job == rhs.job;
0077 }
0078 
0079 namespace
0080 {
0081 
0082 make_comparator_str(ByImportFingerprint, .fingerprint());
0083 
0084 class ImportResultProxyModel : public AbstractKeyListSortFilterProxyModel
0085 {
0086     Q_OBJECT
0087 public:
0088     ImportResultProxyModel(const std::vector<ImportResultData> &results, QObject *parent = nullptr)
0089         : AbstractKeyListSortFilterProxyModel(parent)
0090     {
0091         updateFindCache(results);
0092     }
0093 
0094     ~ImportResultProxyModel() override
0095     {
0096     }
0097 
0098     ImportResultProxyModel *clone() const override
0099     {
0100         // compiler-generated copy ctor is fine!
0101         return new ImportResultProxyModel(*this);
0102     }
0103 
0104     void setImportResults(const std::vector<ImportResultData> &results)
0105     {
0106         updateFindCache(results);
0107         invalidateFilter();
0108     }
0109 
0110 protected:
0111     QVariant data(const QModelIndex &index, int role) const override
0112     {
0113         if (!index.isValid() || role != Qt::ToolTipRole) {
0114             return AbstractKeyListSortFilterProxyModel::data(index, role);
0115         }
0116         const QString fpr = index.data(KeyList::FingerprintRole).toString();
0117         // find information:
0118         const std::vector<Import>::const_iterator it =
0119             Kleo::binary_find(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint<std::less>());
0120         if (it == m_importsByFingerprint.end()) {
0121             return AbstractKeyListSortFilterProxyModel::data(index, role);
0122         } else {
0123             QStringList rv;
0124             const auto ids = m_idsByFingerprint[it->fingerprint()];
0125             rv.reserve(ids.size());
0126             std::copy(ids.cbegin(), ids.cend(), std::back_inserter(rv));
0127             return Formatting::importMetaData(*it, rv);
0128         }
0129     }
0130     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
0131     {
0132         //
0133         // 0. Keep parents of matching children:
0134         //
0135         const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
0136         Q_ASSERT(index.isValid());
0137         for (int i = 0, end = sourceModel()->rowCount(index); i != end; ++i)
0138             if (filterAcceptsRow(i, index)) {
0139                 return true;
0140             }
0141         //
0142         // 1. Check that this is an imported key:
0143         //
0144         const QString fpr = index.data(KeyList::FingerprintRole).toString();
0145 
0146         return std::binary_search(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint<std::less>());
0147     }
0148 
0149 private:
0150     void updateFindCache(const std::vector<ImportResultData> &results)
0151     {
0152         m_importsByFingerprint.clear();
0153         m_idsByFingerprint.clear();
0154         m_results = results;
0155         for (const auto &r : results) {
0156             const std::vector<Import> imports = r.result.imports();
0157             m_importsByFingerprint.insert(m_importsByFingerprint.end(), imports.begin(), imports.end());
0158             for (std::vector<Import>::const_iterator it = imports.begin(), end = imports.end(); it != end; ++it) {
0159                 m_idsByFingerprint[it->fingerprint()].insert(r.id);
0160             }
0161         }
0162         std::sort(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), ByImportFingerprint<std::less>());
0163     }
0164 
0165 private:
0166     mutable std::vector<Import> m_importsByFingerprint;
0167     mutable std::map<const char *, std::set<QString>, ByImportFingerprint<std::less>> m_idsByFingerprint;
0168     std::vector<ImportResultData> m_results;
0169 };
0170 
0171 bool importFailed(const ImportResultData &r)
0172 {
0173     // ignore GPG_ERR_EOF error to handle the "failed" import of files
0174     // without X.509 certificates by gpgsm gracefully
0175     return r.result.error() && r.result.error().code() != GPG_ERR_EOF;
0176 }
0177 
0178 bool importWasCanceled(const ImportResultData &r)
0179 {
0180     return r.result.error().isCanceled();
0181 }
0182 
0183 }
0184 
0185 ImportCertificatesCommand::Private::Private(ImportCertificatesCommand *qq, KeyListController *c)
0186     : Command::Private(qq, c)
0187     , progressWindowTitle{i18nc("@title:window", "Importing Certificates")}
0188     , progressLabelText{i18n("Importing certificates... (this can take a while)")}
0189 {
0190 }
0191 
0192 ImportCertificatesCommand::Private::~Private()
0193 {
0194     if (progressDialog) {
0195         delete progressDialog;
0196     }
0197 }
0198 
0199 #define d d_func()
0200 #define q q_func()
0201 
0202 ImportCertificatesCommand::ImportCertificatesCommand(KeyListController *p)
0203     : Command(new Private(this, p))
0204 {
0205 }
0206 
0207 ImportCertificatesCommand::ImportCertificatesCommand(QAbstractItemView *v, KeyListController *p)
0208     : Command(v, new Private(this, p))
0209 {
0210 }
0211 
0212 ImportCertificatesCommand::~ImportCertificatesCommand() = default;
0213 
0214 static QString format_ids(const std::vector<QString> &ids)
0215 {
0216     QStringList escapedIds;
0217     for (const QString &id : ids) {
0218         if (!id.isEmpty()) {
0219             escapedIds << id.toHtmlEscaped();
0220         }
0221     }
0222     return escapedIds.join(QLatin1StringView("<br>"));
0223 }
0224 
0225 static QString make_tooltip(const std::vector<ImportResultData> &results)
0226 {
0227     if (results.empty()) {
0228         return {};
0229     }
0230 
0231     std::vector<QString> ids;
0232     ids.reserve(results.size());
0233     std::transform(std::begin(results), std::end(results), std::back_inserter(ids), [](const auto &r) {
0234         return r.id;
0235     });
0236     std::sort(std::begin(ids), std::end(ids));
0237     ids.erase(std::unique(std::begin(ids), std::end(ids)), std::end(ids));
0238 
0239     if (ids.size() == 1)
0240         if (ids.front().isEmpty()) {
0241             return {};
0242         } else
0243             return i18nc("@info:tooltip", "Imported Certificates from %1", ids.front().toHtmlEscaped());
0244     else
0245         return i18nc("@info:tooltip", "Imported certificates from these sources:<br/>%1", format_ids(ids));
0246 }
0247 
0248 void ImportCertificatesCommand::Private::setImportResultProxyModel(const std::vector<ImportResultData> &results)
0249 {
0250     if (std::none_of(std::begin(results), std::end(results), [](const auto &r) {
0251             return r.result.numConsidered() > 0;
0252         })) {
0253         return;
0254     }
0255     q->addTemporaryView(i18nc("@title:tab", "Imported Certificates"), new ImportResultProxyModel(results), make_tooltip(results));
0256     if (QTreeView *const tv = qobject_cast<QTreeView *>(parentWidgetOrView())) {
0257         tv->expandAll();
0258     }
0259 }
0260 
0261 int sum(const std::vector<ImportResult> &res, int (ImportResult::*fun)() const)
0262 {
0263     return kdtools::accumulate_transform(res.begin(), res.end(), std::mem_fn(fun), 0);
0264 }
0265 
0266 static QString make_report(const std::vector<ImportResultData> &results, const std::vector<ImportedGroup> &groups)
0267 {
0268     const KLocalizedString normalLine = ki18n("<tr><td align=\"right\">%1</td><td>%2</td></tr>");
0269     const KLocalizedString boldLine = ki18n("<tr><td align=\"right\"><b>%1</b></td><td>%2</td></tr>");
0270     const KLocalizedString headerLine = ki18n("<tr><th colspan=\"2\" align=\"center\">%1</th></tr>");
0271 
0272     std::vector<ImportResult> res;
0273     res.reserve(results.size());
0274     std::transform(std::begin(results), std::end(results), std::back_inserter(res), [](const auto &r) {
0275         return r.result;
0276     });
0277 
0278     const auto numProcessedCertificates = sum(res, &ImportResult::numConsidered);
0279 
0280     QStringList lines;
0281 
0282     if (numProcessedCertificates > 0 || groups.size() == 0) {
0283         lines.push_back(headerLine.subs(i18n("Certificates")).toString());
0284         lines.push_back(normalLine.subs(i18n("Total number processed:")).subs(numProcessedCertificates).toString());
0285         lines.push_back(normalLine.subs(i18n("Imported:")).subs(sum(res, &ImportResult::numImported)).toString());
0286         if (const int n = sum(res, &ImportResult::newSignatures))
0287             lines.push_back(normalLine.subs(i18n("New signatures:")).subs(n).toString());
0288         if (const int n = sum(res, &ImportResult::newUserIDs))
0289             lines.push_back(normalLine.subs(i18n("New user IDs:")).subs(n).toString());
0290         if (const int n = sum(res, &ImportResult::numKeysWithoutUserID))
0291             lines.push_back(normalLine.subs(i18n("Certificates without user IDs:")).subs(n).toString());
0292         if (const int n = sum(res, &ImportResult::newSubkeys))
0293             lines.push_back(normalLine.subs(i18n("New subkeys:")).subs(n).toString());
0294         if (const int n = sum(res, &ImportResult::newRevocations))
0295             lines.push_back(boldLine.subs(i18n("Newly revoked:")).subs(n).toString());
0296         if (const int n = sum(res, &ImportResult::notImported))
0297             lines.push_back(boldLine.subs(i18n("Not imported:")).subs(n).toString());
0298         if (const int n = sum(res, &ImportResult::numUnchanged))
0299             lines.push_back(normalLine.subs(i18n("Unchanged:")).subs(n).toString());
0300         if (const int n = sum(res, &ImportResult::numSecretKeysConsidered))
0301             lines.push_back(normalLine.subs(i18n("Secret keys processed:")).subs(n).toString());
0302         if (const int n = sum(res, &ImportResult::numSecretKeysImported))
0303             lines.push_back(normalLine.subs(i18n("Secret keys imported:")).subs(n).toString());
0304         if (const int n = sum(res, &ImportResult::numSecretKeysConsidered) - sum(res, &ImportResult::numSecretKeysImported)
0305                 - sum(res, &ImportResult::numSecretKeysUnchanged))
0306             if (n > 0)
0307                 lines.push_back(boldLine.subs(i18n("Secret keys <em>not</em> imported:")).subs(n).toString());
0308         if (const int n = sum(res, &ImportResult::numSecretKeysUnchanged))
0309             lines.push_back(normalLine.subs(i18n("Secret keys unchanged:")).subs(n).toString());
0310         if (const int n = sum(res, &ImportResult::numV3KeysSkipped))
0311             lines.push_back(normalLine.subs(i18n("Deprecated PGP-2 keys skipped:")).subs(n).toString());
0312     }
0313 
0314     if (!lines.empty()) {
0315         lines.push_back(headerLine.subs(QLatin1StringView{"&nbsp;"}).toString());
0316     }
0317 
0318     if (groups.size() > 0) {
0319         const auto newGroups = std::count_if(std::begin(groups), std::end(groups), [](const auto &g) {
0320             return g.status == ImportedGroup::Status::New;
0321         });
0322         const auto updatedGroups = groups.size() - newGroups;
0323         lines.push_back(headerLine.subs(i18n("Certificate Groups")).toString());
0324         lines.push_back(normalLine.subs(i18n("Total number processed:")).subs(groups.size()).toString());
0325         lines.push_back(normalLine.subs(i18n("New groups:")).subs(newGroups).toString());
0326         lines.push_back(normalLine.subs(i18n("Updated groups:")).subs(updatedGroups).toString());
0327     }
0328 
0329     return lines.join(QLatin1StringView{});
0330 }
0331 
0332 static bool isImportFromSingleSource(const std::vector<ImportResultData> &res)
0333 {
0334     return (res.size() == 1) || (res.size() == 2 && res[0].id == res[1].id);
0335 }
0336 
0337 static QString make_message_report(const std::vector<ImportResultData> &res, const std::vector<ImportedGroup> &groups)
0338 {
0339     QString report{QLatin1StringView{"<html>"}};
0340     if (res.empty()) {
0341         report += i18n("No imports (should not happen, please report a bug).");
0342     } else {
0343         const QString title = isImportFromSingleSource(res) && !res.front().id.isEmpty() ? i18n("Detailed results of importing %1:", res.front().id)
0344                                                                                          : i18n("Detailed results of import:");
0345         report += QLatin1StringView{"<p>"} + title + QLatin1String{"</p>"};
0346         report += QLatin1StringView{"<p><table width=\"100%\">"};
0347         report += make_report(res, groups);
0348         report += QLatin1StringView{"</table></p>"};
0349     }
0350     report += QLatin1StringView{"</html>"};
0351     return report;
0352 }
0353 
0354 // Returns false on error, true if please certify was shown.
0355 bool ImportCertificatesCommand::Private::showPleaseCertify(const GpgME::Import &imp)
0356 {
0357     if (!Kleo::userHasCertificationKey()) {
0358         qCDebug(KLEOPATRA_LOG) << q << __func__ << "No certification key available";
0359         return false;
0360     }
0361 
0362     const char *fpr = imp.fingerprint();
0363     if (!fpr) {
0364         // WTF
0365         qCWarning(KLEOPATRA_LOG) << "Import without fingerprint";
0366         return false;
0367     }
0368     // Exactly one public key imported. Let's see if it is openpgp. We are async here so
0369     // we can just fetch it.
0370 
0371     auto ctx = wrap_unique(GpgME::Context::createForProtocol(GpgME::OpenPGP));
0372     if (!ctx) {
0373         // WTF
0374         qCWarning(KLEOPATRA_LOG) << "Failed to create OpenPGP proto";
0375         return false;
0376     }
0377     ctx->addKeyListMode(KeyListMode::WithSecret);
0378     GpgME::Error err;
0379     const auto key = ctx->key(fpr, err, false);
0380 
0381     if (key.isNull() || err) {
0382         // No such key most likely not OpenPGP
0383         return false;
0384     }
0385     if (!Kleo::canBeCertified(key)) {
0386         // key is expired or revoked
0387         return false;
0388     }
0389     if (key.hasSecret()) {
0390         qCDebug(KLEOPATRA_LOG) << q << __func__ << "Secret key is available -> skipping certification";
0391         return false;
0392     }
0393 
0394     for (const auto &uid : key.userIDs()) {
0395         if (uid.validity() >= GpgME::UserID::Marginal) {
0396             // Already marginal so don't bug the user
0397             return false;
0398         }
0399     }
0400 
0401     const QStringList suggestions = {
0402         i18n("A phone call to the person."),
0403         i18n("Using a business card."),
0404         i18n("Confirming it on a trusted website."),
0405     };
0406 
0407     auto sel = KMessageBox::questionTwoActions(parentWidgetOrView(),
0408                                                i18n("In order to mark the certificate as valid it needs to be certified.") + QStringLiteral("<br>")
0409                                                    + i18n("Certifying means that you check the Fingerprint.") + QStringLiteral("<br>")
0410                                                    + i18n("Some suggestions to do this are:")
0411                                                    + QStringLiteral("<li><ul>%1").arg(suggestions.join(QStringLiteral("</ul><ul>")))
0412                                                    + QStringLiteral("</ul></li>") + i18n("Do you wish to start this process now?"),
0413                                                i18nc("@title", "You have imported a new certificate (public key)"),
0414                                                KGuiItem(i18nc("@action:button", "Certify")),
0415                                                KStandardGuiItem::cancel(),
0416                                                QStringLiteral("CertifyQuestion"));
0417     if (sel == KMessageBox::ButtonCode::PrimaryAction) {
0418         QEventLoop loop;
0419         auto cmd = new Commands::CertifyCertificateCommand(key);
0420         cmd->setParentWidget(parentWidgetOrView());
0421         connect(cmd, &Command::finished, &loop, &QEventLoop::quit);
0422         QMetaObject::invokeMethod(cmd, &Commands::CertifyCertificateCommand::start, Qt::QueuedConnection);
0423         loop.exec();
0424     }
0425     return true;
0426 }
0427 
0428 namespace
0429 {
0430 /**
0431  * Returns the Import of an OpenPGP key, if a single certificate was imported and this was an OpenPGP key.
0432  * Otherwise, returns a null Import.
0433  */
0434 auto getSingleOpenPGPImport(const std::vector<ImportResultData> &res)
0435 {
0436     static const Import nullImport;
0437     if (!isImportFromSingleSource(res)) {
0438         return nullImport;
0439     }
0440     const auto numImported = std::accumulate(res.cbegin(), res.cend(), 0, [](auto s, const auto &r) {
0441         return s + r.result.numImported();
0442     });
0443     if (numImported > 1) {
0444         return nullImport;
0445     }
0446     if ((res.size() >= 1) && (res[0].protocol == GpgME::OpenPGP) && (res[0].result.numImported() == 1) && (res[0].result.imports().size() == 1)) {
0447         return res[0].result.imports()[0];
0448     } else if ((res.size() == 2) && (res[1].protocol == GpgME::OpenPGP) && (res[1].result.numImported() == 1) && (res[1].result.imports().size() == 1)) {
0449         return res[1].result.imports()[0];
0450     }
0451     return nullImport;
0452 }
0453 
0454 auto consolidatedAuditLogEntries(const std::vector<ImportResultData> &res)
0455 {
0456     static const QString gpg = QStringLiteral("gpg");
0457     static const QString gpgsm = QStringLiteral("gpgsm");
0458 
0459     if (res.size() == 1) {
0460         return res.front().auditLog;
0461     }
0462     QStringList auditLogs;
0463     auto extractAndAnnotateAuditLog = [](const ImportResultData &r) {
0464         QString s;
0465         if (!r.id.isEmpty()) {
0466             const auto program = r.protocol == GpgME::OpenPGP ? gpg : gpgsm;
0467             const auto headerLine = i18nc("file name (imported with gpg/gpgsm)", "%1 (imported with %2)").arg(r.id, program);
0468             s += QStringLiteral("<div><b>%1</b></div>").arg(headerLine);
0469         }
0470         if (r.auditLog.error().code() == GPG_ERR_NO_DATA) {
0471             s += QStringLiteral("<em>") + i18nc("@info", "Audit log is empty.") + QStringLiteral("</em>");
0472         } else if (r.result.error().isCanceled()) {
0473             s += QStringLiteral("<em>") + i18nc("@info", "Import was canceled.") + QStringLiteral("</em>");
0474         } else {
0475             s += r.auditLog.text();
0476         }
0477         return s;
0478     };
0479     std::transform(res.cbegin(), res.cend(), std::back_inserter(auditLogs), extractAndAnnotateAuditLog);
0480     return AuditLogEntry{auditLogs.join(QLatin1StringView{"<hr>"}), Error{}};
0481 }
0482 }
0483 
0484 void ImportCertificatesCommand::Private::showDetails(const std::vector<ImportResultData> &res, const std::vector<ImportedGroup> &groups)
0485 {
0486     const auto singleOpenPGPImport = getSingleOpenPGPImport(res);
0487     if (!singleOpenPGPImport.isNull()) {
0488         if (showPleaseCertify(singleOpenPGPImport)) {
0489             return;
0490         }
0491     }
0492     setImportResultProxyModel(res);
0493     MessageBox::information(parentWidgetOrView(), make_message_report(res, groups), consolidatedAuditLogEntries(res), i18n("Certificate Import Result"));
0494 }
0495 
0496 static QString make_error_message(const Error &err, const QString &id)
0497 {
0498     Q_ASSERT(err);
0499     Q_ASSERT(!err.isCanceled());
0500     if (id.isEmpty()) {
0501         return i18n(
0502             "<qt><p>An error occurred while trying to import the certificate:</p>"
0503             "<p><b>%1</b></p></qt>",
0504             Formatting::errorAsString(err));
0505     } else {
0506         return i18n(
0507             "<qt><p>An error occurred while trying to import the certificate %1:</p>"
0508             "<p><b>%2</b></p></qt>",
0509             id,
0510             Formatting::errorAsString(err));
0511     }
0512 }
0513 
0514 void ImportCertificatesCommand::Private::showError(const ImportResultData &result)
0515 {
0516     MessageBox::error(parentWidgetOrView(), make_error_message(result.result.error(), result.id), result.auditLog);
0517 }
0518 
0519 void ImportCertificatesCommand::Private::setWaitForMoreJobs(bool wait)
0520 {
0521     if (wait == waitForMoreJobs) {
0522         return;
0523     }
0524     waitForMoreJobs = wait;
0525     if (!waitForMoreJobs) {
0526         tryToFinish();
0527     }
0528 }
0529 
0530 void ImportCertificatesCommand::Private::onImportResult(const ImportResult &result, QGpgME::Job *finishedJob)
0531 {
0532     if (!finishedJob) {
0533         finishedJob = qobject_cast<QGpgME::Job *>(q->sender());
0534     }
0535     Q_ASSERT(finishedJob);
0536     qCDebug(KLEOPATRA_LOG) << q << __func__ << finishedJob;
0537 
0538     auto it = std::find_if(std::begin(runningJobs), std::end(runningJobs), [finishedJob](const auto &job) {
0539         return job.job == finishedJob;
0540     });
0541     Q_ASSERT(it != std::end(runningJobs));
0542     if (it == std::end(runningJobs)) {
0543         qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Finished job not found";
0544         return;
0545     }
0546 
0547     Kleo::for_each(it->connections, &disconnectConnection);
0548     it->connections.clear();
0549 
0550     increaseProgressValue();
0551 
0552     const auto job = *it;
0553     addImportResult({job.id, job.protocol, job.type, result, AuditLogEntry::fromJob(finishedJob)}, job);
0554 }
0555 
0556 void ImportCertificatesCommand::Private::addImportResult(const ImportResultData &result, const ImportJobData &job)
0557 {
0558     qCDebug(KLEOPATRA_LOG) << q << __func__ << result.id << "Result:" << Formatting::errorAsString(result.result.error());
0559     results.push_back(result);
0560 
0561     if (importFailed(result)) {
0562         showError(result);
0563     }
0564 
0565     if (job.job) {
0566         const auto count = std::erase(runningJobs, job);
0567         Q_ASSERT(count == 1);
0568     }
0569 
0570     tryToFinish();
0571 }
0572 
0573 static void handleOwnerTrust(const std::vector<ImportResultData> &results, QWidget *dialog)
0574 {
0575     std::unordered_set<std::string> askedAboutFingerprints;
0576     for (const auto &r : results) {
0577         if (r.protocol != GpgME::Protocol::OpenPGP) {
0578             qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping non-OpenPGP import";
0579             continue;
0580         }
0581         const auto imports = r.result.imports();
0582         for (const auto &import : imports) {
0583             if (!(import.status() & (Import::Status::NewKey | Import::Status::ContainedSecretKey))) {
0584                 qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping already known imported public key";
0585                 continue;
0586             }
0587             const char *fpr = import.fingerprint();
0588             if (!fpr) {
0589                 qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping import without fingerprint";
0590                 continue;
0591             }
0592             if (Kleo::contains(askedAboutFingerprints, fpr)) {
0593                 // imports of secret keys can result in multiple Imports for the same key
0594                 qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping import for already handled fingerprint";
0595                 continue;
0596             }
0597 
0598             GpgME::Error err;
0599             auto ctx = wrap_unique(Context::createForProtocol(GpgME::Protocol::OpenPGP));
0600             if (!ctx) {
0601                 qCWarning(KLEOPATRA_LOG) << "Failed to get context";
0602                 continue;
0603             }
0604 
0605             ctx->addKeyListMode(KeyListMode::WithSecret);
0606             const Key toTrustOwner = ctx->key(fpr, err, false);
0607 
0608             if (toTrustOwner.isNull() || !toTrustOwner.hasSecret()) {
0609                 continue;
0610             }
0611             if (toTrustOwner.ownerTrust() == Key::OwnerTrust::Ultimate) {
0612                 qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping key with ultimate ownertrust";
0613                 continue;
0614             }
0615 
0616             const auto toTrustOwnerUserIDs{toTrustOwner.userIDs()};
0617             // ki18n(" ") as initializer because initializing with empty string leads to
0618             // (I18N_EMPTY_MESSAGE)
0619             const KLocalizedString uids = std::accumulate(toTrustOwnerUserIDs.cbegin(),
0620                                                           toTrustOwnerUserIDs.cend(),
0621                                                           KLocalizedString{ki18n(" ")},
0622                                                           [](KLocalizedString temp, const auto &uid) {
0623                                                               return kxi18nc("@info", "%1<item>%2</item>").subs(temp).subs(Formatting::prettyNameAndEMail(uid));
0624                                                           });
0625 
0626             const QString str = xi18nc("@info",
0627                                        "<para>You have imported a certificate with fingerprint</para>"
0628                                        "<para><numid>%1</numid></para>"
0629                                        "<para>"
0630                                        "and user IDs"
0631                                        "<list>%2</list>"
0632                                        "</para>"
0633                                        "<para>Is this your own certificate?</para>",
0634                                        Formatting::prettyID(fpr),
0635                                        uids);
0636 
0637             int k = KMessageBox::questionTwoActionsCancel(dialog,
0638                                                           str,
0639                                                           i18nc("@title:window", "Mark Own Certificate"),
0640                                                           KGuiItem{i18nc("@action:button", "Yes, It's Mine")},
0641                                                           KGuiItem{i18nc("@action:button", "No, It's Not Mine")});
0642             askedAboutFingerprints.insert(fpr);
0643 
0644             if (k == KMessageBox::ButtonCode::PrimaryAction) {
0645                 // To use the ChangeOwnerTrustJob over
0646                 // the CryptoBackendFactory
0647                 const QGpgME::Protocol *const backend = QGpgME::openpgp();
0648 
0649                 if (!backend) {
0650                     qCWarning(KLEOPATRA_LOG) << "Failed to get CryptoBackend";
0651                     return;
0652                 }
0653 
0654                 ChangeOwnerTrustJob *const j = backend->changeOwnerTrustJob();
0655                 j->start(toTrustOwner, Key::Ultimate);
0656             } else if (k == KMessageBox::ButtonCode::Cancel) {
0657                 // do not bother the user with further "Is this yours?" questions
0658                 return;
0659             }
0660         }
0661     }
0662 }
0663 
0664 static void validateImportedCertificate(const GpgME::Import &import)
0665 {
0666     if (const auto fpr = import.fingerprint()) {
0667         auto key = KeyCache::instance()->findByFingerprint(fpr);
0668         if (!key.isNull()) {
0669             // this triggers a keylisting with validation for this certificate
0670             key.update();
0671         } else {
0672             qCWarning(KLEOPATRA_LOG) << __func__ << "Certificate with fingerprint" << fpr << "not found";
0673         }
0674     }
0675 }
0676 
0677 static void handleExternalCMSImports(const std::vector<ImportResultData> &results)
0678 {
0679     // For external CMS Imports we have to manually do a keylist
0680     // with validation to get the intermediate and root ca imported
0681     // automatically if trusted-certs and extra-certs are used.
0682     for (const auto &r : results) {
0683         if (r.protocol == GpgME::CMS && r.type == ImportType::External && !importFailed(r) && !importWasCanceled(r)) {
0684             const auto imports = r.result.imports();
0685             std::for_each(std::begin(imports), std::end(imports), &validateImportedCertificate);
0686         }
0687     }
0688 }
0689 
0690 void ImportCertificatesCommand::Private::processResults()
0691 {
0692     importGroups();
0693 
0694     if (Settings{}.retrieveSignerKeysAfterImport() && !importingSignerKeys) {
0695         importingSignerKeys = true;
0696         const auto missingSignerKeys = getMissingSignerKeyIds(results);
0697         if (!missingSignerKeys.empty()) {
0698             importSignerKeys(missingSignerKeys);
0699             return;
0700         }
0701     }
0702 
0703     handleExternalCMSImports(results);
0704 
0705     // ensure that the progress dialog is closed before we show any other dialogs
0706     setProgressToMaximum();
0707 
0708     handleOwnerTrust(results, parentWidgetOrView());
0709 
0710     showDetails(results, importedGroups);
0711 
0712     auto tv = dynamic_cast<QTreeView *>(view());
0713     if (!tv) {
0714         qCDebug(KLEOPATRA_LOG) << "Failed to find treeview";
0715     } else {
0716         tv->expandAll();
0717     }
0718     finished();
0719 }
0720 
0721 void ImportCertificatesCommand::Private::tryToFinish()
0722 {
0723     qCDebug(KLEOPATRA_LOG) << q << __func__;
0724     if (waitForMoreJobs) {
0725         qCDebug(KLEOPATRA_LOG) << q << __func__ << "Waiting for more jobs -> keep going";
0726         return;
0727     }
0728     if (!runningJobs.empty()) {
0729         qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are unfinished jobs -> keep going";
0730         return;
0731     }
0732     if (!pendingJobs.empty()) {
0733         qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are pending jobs -> start the next one";
0734         auto job = pendingJobs.front();
0735         pendingJobs.pop();
0736         job.job->startNow();
0737         runningJobs.push_back(job);
0738         return;
0739     }
0740 
0741     if (keyListConnection) {
0742         qCWarning(KLEOPATRA_LOG) << q << __func__ << "There is already a valid keyListConnection!";
0743     } else {
0744         auto keyCache = KeyCache::mutableInstance();
0745         keyListConnection = connect(keyCache.get(), &KeyCache::keyListingDone, q, [this]() {
0746             keyCacheUpdated();
0747         });
0748         keyCache->startKeyListing();
0749     }
0750 }
0751 
0752 void ImportCertificatesCommand::Private::keyCacheUpdated()
0753 {
0754     qCDebug(KLEOPATRA_LOG) << q << __func__;
0755     if (!disconnect(keyListConnection)) {
0756         qCWarning(KLEOPATRA_LOG) << q << __func__ << "Failed to disconnect keyListConnection";
0757     }
0758 
0759     keyCacheAutoRefreshSuspension.reset();
0760 
0761     const auto allIds = std::accumulate(std::cbegin(results), std::cend(results), std::set<QString>{}, [](auto allIds, const auto &r) {
0762         allIds.insert(r.id);
0763         return allIds;
0764     });
0765     const auto canceledIds = std::accumulate(std::cbegin(results), std::cend(results), std::set<QString>{}, [](auto canceledIds, const auto &r) {
0766         if (importWasCanceled(r)) {
0767             canceledIds.insert(r.id);
0768         }
0769         return canceledIds;
0770     });
0771     const auto totalConsidered = std::accumulate(std::cbegin(results), std::cend(results), 0, [](auto totalConsidered, const auto &r) {
0772         return totalConsidered + r.result.numConsidered();
0773     });
0774     if (totalConsidered == 0 && canceledIds.size() == allIds.size()) {
0775         // nothing was considered for import and at least one import per id was
0776         // canceled => treat the command as canceled
0777         canceled();
0778         return;
0779     }
0780 
0781     processResults();
0782 }
0783 
0784 static ImportedGroup storeGroup(const KeyGroup &group, const QString &id)
0785 {
0786     const auto status = KeyCache::instance()->group(group.id()).isNull() ? ImportedGroup::Status::New : ImportedGroup::Status::Updated;
0787     if (status == ImportedGroup::Status::New) {
0788         KeyCache::mutableInstance()->insert(group);
0789     } else {
0790         KeyCache::mutableInstance()->update(group);
0791     }
0792     return {id, group, status};
0793 }
0794 
0795 void ImportCertificatesCommand::Private::importGroups()
0796 {
0797     for (const auto &path : filesToImportGroupsFrom) {
0798         const bool certificateImportSucceeded = std::any_of(std::cbegin(results), std::cend(results), [path](const auto &r) {
0799             return r.id == path && !importFailed(r) && !importWasCanceled(r);
0800         });
0801         if (certificateImportSucceeded) {
0802             qCDebug(KLEOPATRA_LOG) << __func__ << "Importing groups from file" << path;
0803             const auto groups = readKeyGroups(path);
0804             std::transform(std::begin(groups), std::end(groups), std::back_inserter(importedGroups), [path](const auto &group) {
0805                 return storeGroup(group, path);
0806             });
0807         }
0808         increaseProgressValue();
0809     }
0810     filesToImportGroupsFrom.clear();
0811 }
0812 
0813 static auto accumulateNewKeys(std::vector<std::string> &fingerprints, const std::vector<GpgME::Import> &imports)
0814 {
0815     return std::accumulate(std::begin(imports), std::end(imports), fingerprints, [](auto fingerprints, const auto &import) {
0816         if (import.status() == Import::NewKey) {
0817             fingerprints.push_back(import.fingerprint());
0818         }
0819         return fingerprints;
0820     });
0821 }
0822 
0823 static auto accumulateNewOpenPGPKeys(const std::vector<ImportResultData> &results)
0824 {
0825     return std::accumulate(std::begin(results), std::end(results), std::vector<std::string>{}, [](auto fingerprints, const auto &r) {
0826         if (r.protocol == GpgME::OpenPGP) {
0827             fingerprints = accumulateNewKeys(fingerprints, r.result.imports());
0828         }
0829         return fingerprints;
0830     });
0831 }
0832 
0833 std::set<QString> ImportCertificatesCommand::Private::getMissingSignerKeyIds(const std::vector<ImportResultData> &results)
0834 {
0835     auto newOpenPGPKeys = KeyCache::instance()->findByFingerprint(accumulateNewOpenPGPKeys(results));
0836     // update all new OpenPGP keys to get information about certifications
0837     std::for_each(std::begin(newOpenPGPKeys), std::end(newOpenPGPKeys), std::mem_fn(&Key::update));
0838     auto missingSignerKeyIds = Kleo::getMissingSignerKeyIds(newOpenPGPKeys);
0839     return missingSignerKeyIds;
0840 }
0841 
0842 void ImportCertificatesCommand::Private::importSignerKeys(const std::set<QString> &keyIds)
0843 {
0844     Q_ASSERT(!keyIds.empty());
0845 
0846     setProgressLabelText(i18np("Fetching 1 signer key... (this can take a while)", "Fetching %1 signer keys... (this can take a while)", keyIds.size()));
0847 
0848     setWaitForMoreJobs(true);
0849     // start one import per key id to allow canceling the key retrieval without
0850     // losing already retrieved keys
0851     for (const auto &keyId : keyIds) {
0852         startImport(GpgME::OpenPGP, {keyId}, QStringLiteral("Retrieve Signer Keys"));
0853     }
0854     setWaitForMoreJobs(false);
0855 }
0856 
0857 static std::unique_ptr<ImportJob> get_import_job(GpgME::Protocol protocol)
0858 {
0859     Q_ASSERT(protocol != UnknownProtocol);
0860     if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
0861         return std::unique_ptr<ImportJob>(backend->importJob());
0862     } else {
0863         return std::unique_ptr<ImportJob>();
0864     }
0865 }
0866 
0867 void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol,
0868                                                      const QByteArray &data,
0869                                                      const QString &id,
0870                                                      [[maybe_unused]] const ImportOptions &options)
0871 {
0872     Q_ASSERT(protocol != UnknownProtocol);
0873 
0874     if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) {
0875         return;
0876     }
0877 
0878     std::unique_ptr<ImportJob> job = get_import_job(protocol);
0879     if (!job.get()) {
0880         nonWorkingProtocols.push_back(protocol);
0881         error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)),
0882               i18n("Certificate Import Failed"));
0883         addImportResult({id, protocol, ImportType::Local, ImportResult{}, AuditLogEntry{}});
0884         return;
0885     }
0886 
0887     keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
0888 
0889     std::vector<QMetaObject::Connection> connections = {
0890         connect(job.get(),
0891                 &AbstractImportJob::result,
0892                 q,
0893                 [this](const GpgME::ImportResult &result) {
0894                     onImportResult(result);
0895                 }),
0896         connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
0897     };
0898 
0899     job->setImportFilter(options.importFilter);
0900     job->setKeyOrigin(options.keyOrigin, options.keyOriginUrl);
0901     const GpgME::Error err = job->startLater(data);
0902     if (err.code()) {
0903         addImportResult({id, protocol, ImportType::Local, ImportResult{err}, AuditLogEntry{}});
0904     } else {
0905         increaseProgressMaximum();
0906         pendingJobs.push({id, protocol, ImportType::Local, job.release(), connections});
0907     }
0908 }
0909 
0910 static std::unique_ptr<ImportFromKeyserverJob> get_import_from_keyserver_job(GpgME::Protocol protocol)
0911 {
0912     Q_ASSERT(protocol != UnknownProtocol);
0913     if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
0914         return std::unique_ptr<ImportFromKeyserverJob>(backend->importFromKeyserverJob());
0915     } else {
0916         return std::unique_ptr<ImportFromKeyserverJob>();
0917     }
0918 }
0919 
0920 void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, const std::vector<Key> &keys, const QString &id)
0921 {
0922     Q_ASSERT(protocol != UnknownProtocol);
0923 
0924     if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) {
0925         return;
0926     }
0927 
0928     std::unique_ptr<ImportFromKeyserverJob> job = get_import_from_keyserver_job(protocol);
0929     if (!job.get()) {
0930         nonWorkingProtocols.push_back(protocol);
0931         error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)),
0932               i18n("Certificate Import Failed"));
0933         addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}});
0934         return;
0935     }
0936 
0937     keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
0938 
0939     std::vector<QMetaObject::Connection> connections = {
0940         connect(job.get(),
0941                 &AbstractImportJob::result,
0942                 q,
0943                 [this](const GpgME::ImportResult &result) {
0944                     onImportResult(result);
0945                 }),
0946         connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
0947     };
0948 
0949     const GpgME::Error err = job->start(keys);
0950     if (err.code()) {
0951         addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}});
0952     } else {
0953         increaseProgressMaximum();
0954         runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections});
0955     }
0956 }
0957 
0958 static auto get_receive_keys_job(GpgME::Protocol protocol)
0959 {
0960     Q_ASSERT(protocol != UnknownProtocol);
0961 
0962     std::unique_ptr<ReceiveKeysJob> job{};
0963     if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
0964         job.reset(backend->receiveKeysJob());
0965     }
0966     return job;
0967 }
0968 
0969 void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, [[maybe_unused]] const QStringList &keyIds, const QString &id)
0970 {
0971     Q_ASSERT(protocol != UnknownProtocol);
0972 
0973     auto job = get_receive_keys_job(protocol);
0974     if (!job.get()) {
0975         qCWarning(KLEOPATRA_LOG) << "Failed to get ReceiveKeysJob for protocol" << Formatting::displayName(protocol);
0976         addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}});
0977         return;
0978     }
0979 
0980     keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
0981 
0982     std::vector<QMetaObject::Connection> connections = {
0983         connect(job.get(),
0984                 &AbstractImportJob::result,
0985                 q,
0986                 [this](const GpgME::ImportResult &result) {
0987                     onImportResult(result);
0988                 }),
0989         connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
0990     };
0991 
0992     const GpgME::Error err = job->start(keyIds);
0993     if (err.code()) {
0994         addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}});
0995     } else {
0996         increaseProgressMaximum();
0997         runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections});
0998     }
0999 }
1000 
1001 void ImportCertificatesCommand::Private::importGroupsFromFile(const QString &filename)
1002 {
1003     increaseProgressMaximum();
1004     filesToImportGroupsFrom.push_back(filename);
1005 }
1006 
1007 void ImportCertificatesCommand::Private::setUpProgressDialog()
1008 {
1009     if (progressDialog) {
1010         return;
1011     }
1012     progressDialog = new QProgressDialog{parentWidgetOrView()};
1013     // use a non-modal progress dialog to avoid reentrancy problems (and crashes) if multiple jobs finish in the same event loop cycle
1014     // (cf. the warning for QProgressDialog::setValue() in the API documentation)
1015     progressDialog->setModal(false);
1016     progressDialog->setWindowTitle(progressWindowTitle);
1017     progressDialog->setLabelText(progressLabelText);
1018     progressDialog->setMinimumDuration(1000);
1019     progressDialog->setMaximum(1);
1020     progressDialog->setValue(0);
1021     connect(progressDialog, &QProgressDialog::canceled, q, &Command::cancel);
1022     connect(q, &Command::finished, progressDialog, [this]() {
1023         progressDialog->accept();
1024     });
1025 }
1026 
1027 void ImportCertificatesCommand::Private::setProgressWindowTitle(const QString &title)
1028 {
1029     if (progressDialog) {
1030         progressDialog->setWindowTitle(title);
1031     } else {
1032         progressWindowTitle = title;
1033     }
1034 }
1035 
1036 void ImportCertificatesCommand::Private::setProgressLabelText(const QString &text)
1037 {
1038     if (progressDialog) {
1039         progressDialog->setLabelText(text);
1040     } else {
1041         progressLabelText = text;
1042     }
1043 }
1044 
1045 void ImportCertificatesCommand::Private::increaseProgressMaximum()
1046 {
1047     setUpProgressDialog();
1048     progressDialog->setMaximum(progressDialog->maximum() + 1);
1049     qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum();
1050 }
1051 
1052 void ImportCertificatesCommand::Private::increaseProgressValue()
1053 {
1054     progressDialog->setValue(progressDialog->value() + 1);
1055     qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum();
1056 }
1057 
1058 void ImportCertificatesCommand::Private::setProgressToMaximum()
1059 {
1060     qCDebug(KLEOPATRA_LOG) << __func__;
1061     progressDialog->setValue(progressDialog->maximum());
1062 }
1063 
1064 void ImportCertificatesCommand::doCancel()
1065 {
1066     const auto jobsToCancel = d->runningJobs;
1067     std::for_each(std::begin(jobsToCancel), std::end(jobsToCancel), [this](const auto &job) {
1068         if (!job.connections.empty()) {
1069             // ignore jobs without connections; they are already completed
1070             qCDebug(KLEOPATRA_LOG) << "Canceling job" << job.job;
1071             job.job->slotCancel();
1072             d->onImportResult(ImportResult{Error::fromCode(GPG_ERR_CANCELED)}, job.job);
1073         }
1074     });
1075 }
1076 
1077 #undef d
1078 #undef q
1079 
1080 #include "importcertificatescommand.moc"
1081 #include "moc_importcertificatescommand.cpp"