diff --git a/src/commands/refreshcertificatecommand.cpp b/src/commands/refreshcertificatecommand.cpp index 702c80696..19bb4f758 100644 --- a/src/commands/refreshcertificatecommand.cpp +++ b/src/commands/refreshcertificatecommand.cpp @@ -1,301 +1,408 @@ /* -*- 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 #if QGPGME_SUPPORTS_KEY_REFRESH #include #include #endif +#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(); #if QGPGME_SUPPORTS_KEY_REFRESH - std::unique_ptr startOpenPGPJob(); + std::unique_ptr startReceiveKeysJob(); std::unique_ptr startSMIMEJob(); #endif - void onOpenPGPJobResult(const ImportResult &result); +#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; #if QGPGME_SUPPORTS_KEY_REFRESH QPointer job; #endif + 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; } #if QGPGME_SUPPORTS_KEY_REFRESH std::unique_ptr refreshJob; switch (key.protocol()) { case GpgME::OpenPGP: - refreshJob = startOpenPGPJob(); + refreshJob = startReceiveKeysJob(); break; case GpgME::CMS: refreshJob = startSMIMEJob(); break; default:; // cannot happen ;-) } if (!refreshJob) { finished(); return; } job = refreshJob.release(); #else error(i18n("The backend does not support updating individual certificates.")); finished(); #endif } void RefreshCertificateCommand::Private::cancel() { #if QGPGME_SUPPORTS_KEY_REFRESH if (job) { job->slotCancel(); } job.clear(); #endif } #if QGPGME_SUPPORTS_KEY_REFRESH -std::unique_ptr RefreshCertificateCommand::Private::startOpenPGPJob() +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) { - onOpenPGPJobResult(result); + onReceiveKeysJobResult(result); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(refreshJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(refreshJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif 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); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(refreshJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(refreshJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif const GpgME::Error err = refreshJob->start({key}); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Updating certificate...")); return refreshJob; } #endif +#if QGPGME_SUPPORTS_WKD_REFRESH_JOB +std::unique_ptr RefreshCertificateCommand::Private::startWKDRefreshJob() +{ + // 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); + + const GpgME::Error 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 += QLatin1String{"

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

"} + text + QLatin1String{"

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

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

"}; } return text; } } -void RefreshCertificateCommand::Private::onOpenPGPJobResult(const ImportResult &result) +void RefreshCertificateCommand::Private::onReceiveKeysJobResult(const ImportResult &result) { - if (result.error()) { - showError(result.error()); + receiveKeysResult = result; + + if (result.error().isCanceled()) { finished(); return; } - if (!result.error().isCanceled()) { +#if QGPGME_SUPPORTS_WKD_REFRESH_JOB + std::unique_ptr refreshJob = startWKDRefreshJob(); + if (!refreshJob) { + showOpenPGPResult(); + return; + } + job = refreshJob.release(); +#else + if (result.error()) { + showError(result.error()); + } else { information(informationOnChanges(result), 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()) { + showError(receiveKeysResult.error()); + } else { + information(informationOnChanges(receiveKeysResult), i18nc("@title:window", "Key Updated")); + } + finished(); + return; + } + + if (receiveKeysResult.error() && 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 += QLatin1String{"

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

"}; + if (receiveKeysResult.error()) { + text += xi18nc("@info", "The update failed: %1", Formatting::errorAsString(receiveKeysResult.error())); + } else { + text += informationOnChanges(receiveKeysResult); + } + + text += QLatin1String{"

"} + 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"