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{" "}).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"