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

0001 /*
0002     commands/certifygroupcommand.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2023 g10 Code GmbH
0006     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "certifygroupcommand.h"
0012 #include "command_p.h"
0013 
0014 #include <commands/exportopenpgpcertstoservercommand.h>
0015 #include <dialogs/certifycertificatedialog.h>
0016 #include <utils/tags.h>
0017 
0018 #include <Libkleo/Algorithm>
0019 #include <Libkleo/Formatting>
0020 #include <Libkleo/KeyGroup>
0021 #include <Libkleo/KeyHelpers>
0022 
0023 #include <QGpgME/Protocol>
0024 #include <QGpgME/SignKeyJob>
0025 
0026 #include <QDate>
0027 #include <QProgressDialog>
0028 
0029 #include <gpgme++/key.h>
0030 
0031 using namespace Kleo;
0032 using namespace Kleo::Commands;
0033 using namespace GpgME;
0034 
0035 namespace
0036 {
0037 struct CertificationResultData {
0038     std::vector<UserID> userIds;
0039     GpgME::Error error;
0040 };
0041 }
0042 
0043 class CertifyGroupCommand::Private : public Command::Private
0044 {
0045     friend class ::Kleo::CertifyGroupCommand;
0046     CertifyGroupCommand *q_func() const
0047     {
0048         return static_cast<CertifyGroupCommand *>(q);
0049     }
0050 
0051 public:
0052     explicit Private(CertifyGroupCommand *qq);
0053     ~Private() override;
0054 
0055     void start();
0056 
0057 private:
0058     void showDialog();
0059     void certifyCertificates();
0060     void setUpProgressDialog(int numberOfKeysToCertify);
0061     void startNextCertification();
0062     void createJob();
0063     void slotResult(const Error &err);
0064     void wrapUp();
0065 
0066 private:
0067     KeyGroup group;
0068     std::vector<Key> certificates;
0069     QPointer<CertifyCertificateDialog> dialog;
0070     QPointer<QProgressDialog> progressDialog;
0071     std::vector<UserID> userIdsToCertify;
0072     struct {
0073         Key certificationKey;
0074         QDate expirationDate;
0075         QString tags;
0076         bool exportable = false;
0077         bool sendToServer = false;
0078     } certificationOptions;
0079     struct {
0080         std::vector<UserID> userIds;
0081     } jobData;
0082     QPointer<QGpgME::SignKeyJob> job;
0083     std::vector<CertificationResultData> results;
0084 };
0085 
0086 CertifyGroupCommand::Private *CertifyGroupCommand::d_func()
0087 {
0088     return static_cast<Private *>(d.get());
0089 }
0090 const CertifyGroupCommand::Private *CertifyGroupCommand::d_func() const
0091 {
0092     return static_cast<const Private *>(d.get());
0093 }
0094 
0095 #define d d_func()
0096 #define q q_func()
0097 
0098 CertifyGroupCommand::Private::Private(CertifyGroupCommand *qq)
0099     : Command::Private(qq)
0100 {
0101 }
0102 
0103 CertifyGroupCommand::Private::~Private() = default;
0104 
0105 void CertifyGroupCommand::Private::start()
0106 {
0107     if (!group.isNull()) {
0108         const auto &groupKeys = group.keys();
0109         certificates = std::vector<GpgME::Key>(groupKeys.begin(), groupKeys.end());
0110     }
0111     if (certificates.empty()) {
0112         finished();
0113         return;
0114     }
0115     if (!allKeysHaveProtocol(certificates, GpgME::OpenPGP)) {
0116         const auto title = i18nc("@title:window", "Group Cannot Be Certified");
0117         const auto message = i18nc("@info", "This group contains S/MIME certificates which cannot be certified.");
0118         information(message, title);
0119         finished();
0120         return;
0121     }
0122 
0123     showDialog();
0124 }
0125 
0126 void CertifyGroupCommand::Private::showDialog()
0127 {
0128     dialog = new CertifyCertificateDialog;
0129     dialog->setAttribute(Qt::WA_DeleteOnClose);
0130     applyWindowID(dialog);
0131 
0132     connect(dialog, &QDialog::accepted, q, [this]() {
0133         certifyCertificates();
0134     });
0135     connect(dialog, &QDialog::rejected, q, [this]() {
0136         canceled();
0137     });
0138 
0139     if (!group.isNull()) {
0140         dialog->setGroupName(group.name());
0141     }
0142     dialog->setCertificatesToCertify(certificates);
0143     dialog->show();
0144 }
0145 
0146 void CertifyGroupCommand::Private::certifyCertificates()
0147 {
0148     userIdsToCertify = dialog->selectedUserIDs();
0149     if (userIdsToCertify.empty()) {
0150         canceled();
0151         return;
0152     }
0153     certificationOptions.certificationKey = dialog->selectedSecretKey();
0154     certificationOptions.expirationDate = dialog->expirationDate();
0155     certificationOptions.tags = dialog->tags();
0156     certificationOptions.exportable = dialog->exportableCertificationSelected();
0157     certificationOptions.sendToServer = dialog->sendToServer();
0158 
0159     setUpProgressDialog(userIdsToCertify.size());
0160     startNextCertification();
0161 }
0162 
0163 void CertifyGroupCommand::Private::setUpProgressDialog(int numberOfKeysToCertify)
0164 {
0165     if (progressDialog) {
0166         return;
0167     }
0168     progressDialog = new QProgressDialog{parentWidgetOrView()};
0169     progressDialog->setAttribute(Qt::WA_DeleteOnClose);
0170     progressDialog->setModal(true);
0171     progressDialog->setWindowTitle(i18nc("@title:window", "Certify Certificates"));
0172     progressDialog->setLabelText(i18nc("@info:progress", "Certifying certificates ..."));
0173     progressDialog->setMinimumDuration(1000);
0174     progressDialog->setMaximum(numberOfKeysToCertify);
0175     progressDialog->setValue(0);
0176     connect(progressDialog, &QProgressDialog::canceled, q, &Command::cancel);
0177     connect(q, &Command::finished, progressDialog, [this]() {
0178         progressDialog->accept();
0179     });
0180 }
0181 
0182 void CertifyGroupCommand::Private::startNextCertification()
0183 {
0184     Q_ASSERT(!userIdsToCertify.empty());
0185 
0186     const auto nextKey = userIdsToCertify.front().parent();
0187     // for now we only deal with primary user IDs
0188     jobData.userIds = {userIdsToCertify.front()};
0189     userIdsToCertify.erase(userIdsToCertify.begin());
0190     const std::vector<unsigned int> userIdIndexes = {0};
0191 
0192     createJob();
0193     job->setUserIDsToSign(userIdIndexes);
0194     if (const Error err = job->start(nextKey)) {
0195         QMetaObject::invokeMethod(
0196             q,
0197             [this, err]() {
0198                 slotResult(err);
0199             },
0200             Qt::QueuedConnection);
0201     }
0202 }
0203 
0204 void CertifyGroupCommand::Private::createJob()
0205 {
0206     Q_ASSERT(!job);
0207 
0208     std::unique_ptr<QGpgME::SignKeyJob> newJob{QGpgME::openpgp()->signKeyJob()};
0209     newJob->setDupeOk(true);
0210     newJob->setSigningKey(certificationOptions.certificationKey);
0211     newJob->setExportable(certificationOptions.exportable);
0212     if (!certificationOptions.tags.isEmpty()) {
0213         // do not set an empty remark to avoid an empty signature notation (GnuPG bug T5142)
0214         newJob->setRemark(certificationOptions.tags);
0215     }
0216     if (!certificationOptions.expirationDate.isNull()) {
0217         newJob->setExpirationDate(certificationOptions.expirationDate);
0218     }
0219     connect(newJob.get(), &QGpgME::SignKeyJob::result, q, [this](const GpgME::Error &result) {
0220         slotResult(result);
0221     });
0222 
0223     job = newJob.release();
0224 }
0225 
0226 void CertifyGroupCommand::Private::slotResult(const Error &err)
0227 {
0228     results.push_back({
0229         jobData.userIds,
0230         err,
0231     });
0232     progressDialog->setValue(results.size());
0233 
0234     if (err.isCanceled()) {
0235         finished();
0236     } else if (err && (results.size() == 1) //
0237                && ((err.sourceID() == GPG_ERR_SOURCE_PINENTRY) //
0238                    || (err.code() == GPG_ERR_BAD_PASSPHRASE))) {
0239         // abort the certification process on certain errors during the first
0240         // certification, e.g. bad passphrase or pinentry timeout, where it's
0241         // better to restart the whole process instead of continuing with the
0242         // next certificate
0243         error(xi18nc("@info",
0244                      "<para>The certification of the certificates failed.</para>"
0245                      "<para>Error: <message>%1</message></para>",
0246                      Formatting::errorAsString(results.front().error)));
0247         finished();
0248     } else if (!userIdsToCertify.empty()) {
0249         job.clear();
0250         jobData.userIds.clear();
0251         startNextCertification();
0252     } else {
0253         wrapUp();
0254     }
0255 }
0256 
0257 static QString resultSummary(const std::vector<CertificationResultData> &results)
0258 {
0259     Q_ASSERT(!results.empty());
0260 
0261     const int totalCount = results.size();
0262     const int successCount = Kleo::count_if(results, [](const auto &result) {
0263         return !result.error;
0264     });
0265 
0266     if (successCount == totalCount) {
0267         return i18nc("@info", "All certificates were certified successfully.");
0268     }
0269     if (successCount == 0) {
0270         // we assume that all attempted certifications failed for the same reason
0271         return xi18nc("@info",
0272                       "<para>The certification of all certificates failed.</para>"
0273                       "<para>Error: <message>%1</message></para>",
0274                       Formatting::errorAsString(results.front().error));
0275     }
0276     return i18ncp("@info", //
0277                   "1 of %2 certificates was certified successfully.",
0278                   "%1 of %2 certificates were certified successfully.",
0279                   successCount,
0280                   totalCount);
0281 }
0282 
0283 void CertifyGroupCommand::Private::wrapUp()
0284 {
0285     Q_ASSERT(userIdsToCertify.empty());
0286     Q_ASSERT(!results.empty());
0287 
0288     const int successCount = Kleo::count_if(results, [](const auto &result) {
0289         return !result.error;
0290     });
0291     const bool sendToServer = (successCount > 0) && certificationOptions.exportable && certificationOptions.sendToServer;
0292 
0293     QString message = QLatin1StringView{"<p>"} + resultSummary(results) + QLatin1String{"</p>"};
0294     if (sendToServer) {
0295         message += i18nc("@info", "<p>Next the certified certificates will be uploaded to the configured certificate directory.</p>");
0296     }
0297     const auto failedUserIdsInfo = std::accumulate(results.cbegin(), results.cend(), QStringList{}, [](auto failedUserIds, const auto &result) {
0298         if (result.error) {
0299             failedUserIds.push_back(i18nc("A user ID (an error description)",
0300                                           "%1 (%2)",
0301                                           Formatting::formatForComboBox(result.userIds.front().parent()),
0302                                           Formatting::errorAsString(result.error)));
0303         }
0304         return failedUserIds;
0305     });
0306 
0307     if (successCount > 0) {
0308         if (failedUserIdsInfo.size() > 0) {
0309             message += i18nc("@info", "<p>Certifying the following certificates failed:</p>");
0310         }
0311         informationList(message, failedUserIdsInfo, i18nc("@title:window", "Certification Completed"));
0312     } else {
0313         error(message);
0314     }
0315 
0316     if (sendToServer) {
0317         const auto certificatesToSendToServer = std::accumulate(results.cbegin(), results.cend(), std::vector<Key>{}, [](auto keys, const auto &result) {
0318             if (!result.error) {
0319                 keys.push_back(result.userIds.front().parent());
0320             }
0321             return keys;
0322         });
0323         const auto cmd = new ExportOpenPGPCertsToServerCommand(certificatesToSendToServer);
0324         cmd->start();
0325     }
0326 
0327     if (!certificationOptions.tags.isEmpty()) {
0328         Tags::enableTags();
0329     }
0330     finished();
0331 }
0332 
0333 CertifyGroupCommand::CertifyGroupCommand(const KeyGroup &group)
0334     : Command{new Private{this}}
0335 {
0336     d->group = group;
0337 }
0338 
0339 CertifyGroupCommand::~CertifyGroupCommand() = default;
0340 
0341 void CertifyGroupCommand::doStart()
0342 {
0343     d->start();
0344 }
0345 
0346 void CertifyGroupCommand::doCancel()
0347 {
0348     if (d->dialog) {
0349         d->dialog->close();
0350     }
0351     if (d->job) {
0352         d->job->slotCancel();
0353     }
0354 }
0355 
0356 #undef d
0357 #undef q
0358 
0359 #include "moc_certifygroupcommand.cpp"