diff --git a/src/commands/refreshcertificatecommand.cpp b/src/commands/refreshcertificatecommand.cpp index 5c7c45251..84c76f68c 100644 --- a/src/commands/refreshcertificatecommand.cpp +++ b/src/commands/refreshcertificatecommand.cpp @@ -1,415 +1,426 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/refreshcertificatecommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "command_p.h" #include "refreshcertificatecommand.h" #include #include #include #include #include #include #include #include #if QGPGME_SUPPORTS_WKD_REFRESH_JOB #include #endif #include #include "kleopatra_debug.h" using namespace Kleo; using namespace GpgME; class RefreshCertificateCommand::Private : public Command::Private { friend class ::RefreshCertificateCommand; RefreshCertificateCommand *q_func() const { return static_cast(q); } public: explicit Private(RefreshCertificateCommand *qq); ~Private() override; void start(); void cancel(); std::unique_ptr startReceiveKeysJob(); std::unique_ptr startSMIMEJob(); #if QGPGME_SUPPORTS_WKD_REFRESH_JOB std::unique_ptr startWKDRefreshJob(); #endif void onReceiveKeysJobResult(const ImportResult &result); void onWKDRefreshJobResult(const ImportResult &result); void onSMIMEJobResult(const Error &err); void showOpenPGPResult(); void showError(const Error &err); private: Key key; QPointer job; ImportResult receiveKeysResult; ImportResult wkdRefreshResult; }; RefreshCertificateCommand::Private *RefreshCertificateCommand::d_func() { return static_cast(d.get()); } const RefreshCertificateCommand::Private *RefreshCertificateCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() RefreshCertificateCommand::Private::Private(RefreshCertificateCommand *qq) : Command::Private{qq} { } RefreshCertificateCommand::Private::~Private() = default; namespace { Key getKey(const std::vector &keys) { if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Expected exactly one key, but got" << keys.size(); return {}; } const Key key = keys.front(); if (key.protocol() == GpgME::UnknownProtocol) { qCWarning(KLEOPATRA_LOG) << "Key has unknown protocol"; return {}; } return key; } } void RefreshCertificateCommand::Private::start() { key = getKey(keys()); if (key.isNull()) { finished(); return; } std::unique_ptr refreshJob; switch (key.protocol()) { case GpgME::OpenPGP: { if (haveKeyserverConfigured()) { refreshJob = startReceiveKeysJob(); } else { QMetaObject::invokeMethod( q, [this]() { // use GPG_ERR_USER_1 to signal skipped key server lookup onReceiveKeysJobResult(ImportResult{Error::fromCode(GPG_ERR_USER_1)}); }, Qt::QueuedConnection); return; } break; } case GpgME::CMS: refreshJob = startSMIMEJob(); break; default:; // cannot happen ;-) } if (!refreshJob) { finished(); return; } job = refreshJob.release(); } void RefreshCertificateCommand::Private::cancel() { if (job) { job->slotCancel(); } job.clear(); } std::unique_ptr RefreshCertificateCommand::Private::startReceiveKeysJob() { std::unique_ptr refreshJob{QGpgME::openpgp()->receiveKeysJob()}; Q_ASSERT(refreshJob); connect(refreshJob.get(), &QGpgME::ReceiveKeysJob::result, q, [this](const GpgME::ImportResult &result) { onReceiveKeysJobResult(result); }); connect(refreshJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); const GpgME::Error err = refreshJob->start({QString::fromLatin1(key.primaryFingerprint())}); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Updating key...")); return refreshJob; } std::unique_ptr RefreshCertificateCommand::Private::startSMIMEJob() { std::unique_ptr refreshJob{QGpgME::smime()->refreshKeysJob()}; Q_ASSERT(refreshJob); connect(refreshJob.get(), &QGpgME::RefreshKeysJob::result, q, [this](const GpgME::Error &err) { onSMIMEJobResult(err); }); connect(refreshJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); const GpgME::Error err = refreshJob->start({key}); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Updating certificate...")); return refreshJob; } #if QGPGME_SUPPORTS_WKD_REFRESH_JOB std::unique_ptr RefreshCertificateCommand::Private::startWKDRefreshJob() { if (!Settings{}.queryWKDsForAllUserIDs()) { // check if key is eligible for WKD refresh, i.e. if any user ID has WKD as origin const auto userIds = key.userIDs(); const auto eligibleForWKDRefresh = std::any_of(userIds.begin(), userIds.end(), [](const auto &userId) { return !userId.isRevoked() && !userId.addrSpec().empty() && userId.origin() == Key::OriginWKD; }); if (!eligibleForWKDRefresh) { wkdRefreshResult = ImportResult{Error::fromCode(GPG_ERR_USER_1)}; return {}; } } std::unique_ptr refreshJob{QGpgME::openpgp()->wkdRefreshJob()}; Q_ASSERT(refreshJob); connect(refreshJob.get(), &QGpgME::WKDRefreshJob::result, q, [this](const GpgME::ImportResult &result) { onWKDRefreshJobResult(result); }); connect(refreshJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); Error err; if (Settings{}.queryWKDsForAllUserIDs()) { err = refreshJob->start(key.userIDs()); } else { err = refreshJob->start({key}); } if (err) { wkdRefreshResult = ImportResult{err}; return {}; } Q_EMIT q->info(i18nc("@info:status", "Updating key...")); return refreshJob; } #endif namespace { static auto informationOnChanges(const ImportResult &result) { QString text; // if additional keys have been retrieved via WKD, then most of the below // details are just a guess and may concern the additional keys instead of // the refresh keys; this could only be clarified by a thorough comparison of // unrefreshed and refreshed key if (result.numUnchanged() == result.numConsidered()) { // if numUnchanged < numConsidered, then it is not clear whether the refreshed key // hasn't changed or whether another key retrieved via WKD hasn't changed text = i18n("The key hasn't changed."); } else if (result.newRevocations() > 0) { // it is possible that a revoked key has been newly imported via WKD, // but it is much more likely that the refreshed key was revoked text = i18n("The key has been revoked."); } else { // it doesn't make much sense to list below details if the key has been revoked text = i18n("The key has been updated."); QStringList details; if (result.newUserIDs() > 0) { details.push_back(i18n("New user IDs: %1", result.newUserIDs())); } if (result.newSubkeys() > 0) { details.push_back(i18n("New subkeys: %1", result.newSubkeys())); } if (result.newSignatures() > 0) { details.push_back(i18n("New signatures: %1", result.newSignatures())); } if (!details.empty()) { text += QLatin1StringView{"

"} + details.join(QLatin1String{"
"}); } } text = QLatin1StringView{"

"} + text + QLatin1String{"

"}; if (result.numImported() > 0) { text += QLatin1StringView{"

"} + i18np("Additionally, one new key has been retrieved.", "Additionally, %1 new keys have been retrieved.", result.numImported()) + QLatin1StringView{"

"}; } return text; } } void RefreshCertificateCommand::Private::onReceiveKeysJobResult(const ImportResult &result) { receiveKeysResult = result; if (result.error().isCanceled()) { finished(); return; } #if QGPGME_SUPPORTS_WKD_REFRESH_JOB std::unique_ptr refreshJob = startWKDRefreshJob(); if (!refreshJob) { showOpenPGPResult(); return; } job = refreshJob.release(); #else if (receiveKeysResult.error()) { if (receiveKeysResult.error().code() == GPG_ERR_USER_1) { information(i18nc("@info", "The update was skipped because no keyserver is configured."), i18nc("@title:window", "Update Skipped")); } else { showError(receiveKeysResult.error()); } } else { information(informationOnChanges(receiveKeysResult), i18nc("@title:window", "Key Updated")); } finished(); #endif } void RefreshCertificateCommand::Private::onWKDRefreshJobResult(const ImportResult &result) { wkdRefreshResult = result; showOpenPGPResult(); } void RefreshCertificateCommand::Private::onSMIMEJobResult(const Error &err) { if (err) { showError(err); finished(); return; } if (!err.isCanceled()) { information(i18nc("@info", "The certificate has been updated."), i18nc("@title:window", "Certificate Updated")); } finished(); } void RefreshCertificateCommand::Private::showOpenPGPResult() { if (wkdRefreshResult.error().code() == GPG_ERR_USER_1 || wkdRefreshResult.error().isCanceled()) { if (receiveKeysResult.error()) { if (receiveKeysResult.error().code() == GPG_ERR_USER_1) { information(i18nc("@info", "The update was skipped because no keyserver is configured."), i18nc("@title:window", "Update Skipped")); } else { - showError(receiveKeysResult.error()); + if (receiveKeysResult.error().code() == GPG_ERR_NO_DATA) { + information(i18nc("@info", "The certificate was not found on keyserver %1.", keyserver())); + } else if (receiveKeysResult.error().code() == GPG_ERR_EHOSTUNREACH) { + error(xi18nc("@info", + "Keyserver %1 is not reachable." + "%2", + keyserver(), + Formatting::errorAsString(receiveKeysResult.error())), + i18nc("@title:window", "Update Failed")); + } else { + showError(receiveKeysResult.error()); + } } } else { information(informationOnChanges(receiveKeysResult), i18nc("@title:window", "Key Updated")); } finished(); return; } if (receiveKeysResult.error() && (receiveKeysResult.error().code() != GPG_ERR_USER_1) && wkdRefreshResult.error()) { error(xi18nc("@info", "Updating the certificate from a keyserver, an LDAP server, or Active Directory failed:" "%1" "Updating the certificate via Web Key Directory failed:" "%2", Formatting::errorAsString(receiveKeysResult.error()), Formatting::errorAsString(wkdRefreshResult.error())), i18nc("@title:window", "Update Failed")); finished(); return; } QString text; text += QLatin1StringView{"

"} + i18nc("@info", "Result of update from keyserver, LDAP server, or Active Directory") + QLatin1String{"

"}; if (receiveKeysResult.error()) { if (receiveKeysResult.error().code() == GPG_ERR_USER_1) { text += xi18nc("@info", "The update was skipped because no keyserver is configured."); } else { text += xi18nc("@info", "The update failed: %1", Formatting::errorAsString(receiveKeysResult.error())); } } else { text += informationOnChanges(receiveKeysResult); } text += QLatin1StringView{"

"} + i18nc("@info", "Result of update via Web Key Directory") + QLatin1String{"

"}; if (wkdRefreshResult.error()) { text += xi18nc("@info", "The update failed: %1", Formatting::errorAsString(wkdRefreshResult.error())); } else { text += informationOnChanges(wkdRefreshResult); } information(text, i18nc("@title:window", "Key Updated")); finished(); } void RefreshCertificateCommand::Private::showError(const Error &err) { error(xi18nc("@info", "An error occurred while updating the certificate:" "%1", Formatting::errorAsString(err)), i18nc("@title:window", "Update Failed")); } RefreshCertificateCommand::RefreshCertificateCommand(const GpgME::Key &key) : Command{key, new Private{this}} { } RefreshCertificateCommand::~RefreshCertificateCommand() = default; void RefreshCertificateCommand::doStart() { d->start(); } void RefreshCertificateCommand::doCancel() { d->cancel(); } #undef d #undef q #include "moc_refreshcertificatecommand.cpp"