File indexing completed on 2024-06-02 05:24:12

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     exportcertificatecommand.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
0006     SPDX-FileCopyrightText: 2021 g10 Code GmbH
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include <config-kleopatra.h>
0012 
0013 #include "exportcertificatecommand.h"
0014 #include "fileoperationspreferences.h"
0015 
0016 #include "command_p.h"
0017 
0018 #include <utils/applicationstate.h>
0019 #include <utils/filedialog.h>
0020 
0021 #include <Libkleo/Algorithm>
0022 #include <Libkleo/Classify>
0023 #include <Libkleo/Formatting>
0024 #include <Libkleo/KeyHelpers>
0025 
0026 #include <QGpgME/ExportJob>
0027 #include <QGpgME/Protocol>
0028 
0029 #include <gpgme++/key.h>
0030 
0031 #include <KLocalizedString>
0032 #include <QSaveFile>
0033 
0034 #include <QFileInfo>
0035 #include <QMap>
0036 #include <QPointer>
0037 
0038 #include <algorithm>
0039 #include <vector>
0040 
0041 using namespace Kleo;
0042 using namespace GpgME;
0043 using namespace QGpgME;
0044 
0045 class ExportCertificateCommand::Private : public Command::Private
0046 {
0047     friend class ::ExportCertificateCommand;
0048     ExportCertificateCommand *q_func() const
0049     {
0050         return static_cast<ExportCertificateCommand *>(q);
0051     }
0052 
0053 public:
0054     explicit Private(ExportCertificateCommand *qq, KeyListController *c);
0055     ~Private() override;
0056     void startExportJob(GpgME::Protocol protocol, const std::vector<Key> &keys);
0057     void cancelJobs();
0058     void exportResult(const GpgME::Error &, const QByteArray &);
0059     void showError(const GpgME::Error &error);
0060 
0061     bool confirmExport(const std::vector<Key> &pgpKeys);
0062     bool requestFileNames(GpgME::Protocol prot);
0063     void finishedIfLastJob();
0064 
0065 private:
0066     QMap<GpgME::Protocol, QString> fileNames;
0067     uint jobsPending = 0;
0068     QMap<QObject *, QString> outFileForSender;
0069     QPointer<ExportJob> cmsJob;
0070     QPointer<ExportJob> pgpJob;
0071 };
0072 
0073 ExportCertificateCommand::Private *ExportCertificateCommand::d_func()
0074 {
0075     return static_cast<Private *>(d.get());
0076 }
0077 const ExportCertificateCommand::Private *ExportCertificateCommand::d_func() const
0078 {
0079     return static_cast<const Private *>(d.get());
0080 }
0081 
0082 #define d d_func()
0083 #define q q_func()
0084 
0085 ExportCertificateCommand::Private::Private(ExportCertificateCommand *qq, KeyListController *c)
0086     : Command::Private(qq, c)
0087 {
0088 }
0089 
0090 ExportCertificateCommand::Private::~Private()
0091 {
0092 }
0093 
0094 ExportCertificateCommand::ExportCertificateCommand(KeyListController *p)
0095     : Command(new Private(this, p))
0096 {
0097 }
0098 
0099 ExportCertificateCommand::ExportCertificateCommand(QAbstractItemView *v, KeyListController *p)
0100     : Command(v, new Private(this, p))
0101 {
0102 }
0103 
0104 ExportCertificateCommand::ExportCertificateCommand(const Key &key)
0105     : Command(key, new Private(this, nullptr))
0106 {
0107 }
0108 
0109 ExportCertificateCommand::~ExportCertificateCommand()
0110 {
0111 }
0112 
0113 void ExportCertificateCommand::setOpenPGPFileName(const QString &fileName)
0114 {
0115     if (!d->jobsPending) {
0116         d->fileNames[OpenPGP] = fileName;
0117     }
0118 }
0119 
0120 QString ExportCertificateCommand::openPGPFileName() const
0121 {
0122     return d->fileNames[OpenPGP];
0123 }
0124 
0125 void ExportCertificateCommand::setX509FileName(const QString &fileName)
0126 {
0127     if (!d->jobsPending) {
0128         d->fileNames[CMS] = fileName;
0129     }
0130 }
0131 
0132 QString ExportCertificateCommand::x509FileName() const
0133 {
0134     return d->fileNames[CMS];
0135 }
0136 
0137 void ExportCertificateCommand::doStart()
0138 {
0139     if (d->keys().empty()) {
0140         d->finished();
0141         return;
0142     }
0143 
0144     const auto keys = Kleo::partitionKeysByProtocol(d->keys());
0145 
0146     if (!keys.openpgp.empty() && !d->confirmExport(keys.openpgp)) {
0147         d->canceled();
0148         return;
0149     }
0150 
0151     const bool haveBoth = !keys.cms.empty() && !keys.openpgp.empty();
0152     const GpgME::Protocol prot = haveBoth ? UnknownProtocol : (!keys.cms.empty() ? CMS : OpenPGP);
0153     if (!d->requestFileNames(prot)) {
0154         d->canceled();
0155         return;
0156     }
0157 
0158     if (!keys.openpgp.empty()) {
0159         d->startExportJob(GpgME::OpenPGP, keys.openpgp);
0160     }
0161     if (!keys.cms.empty()) {
0162         d->startExportJob(GpgME::CMS, keys.cms);
0163     }
0164 }
0165 
0166 bool ExportCertificateCommand::Private::confirmExport(const std::vector<Key> &pgpKeys)
0167 {
0168     auto notCertifiedKeys = std::accumulate(pgpKeys.cbegin(), pgpKeys.cend(), QStringList{}, [](auto keyNames, const auto &key) {
0169         const bool allValidUserIDsAreCertifiedByUser = Kleo::all_of(key.userIDs(), [](const UserID &userId) {
0170             return userId.isBad() || Kleo::userIDIsCertifiedByUser(userId);
0171         });
0172         if (!allValidUserIDsAreCertifiedByUser) {
0173             keyNames.push_back(Formatting::formatForComboBox(key));
0174         }
0175         return keyNames;
0176     });
0177     if (!notCertifiedKeys.empty()) {
0178         if (pgpKeys.size() == 1) {
0179             const auto answer = KMessageBox::warningContinueCancel( //
0180                 parentWidgetOrView(),
0181                 xi18nc("@info",
0182                        "<para>You haven't certified all valid user IDs of this certificate "
0183                        "with an exportable certification. People relying on your certifications "
0184                        "may not be able to verify the certificate.</para>"
0185                        "<para>Do you want to continue the export?</para>"),
0186                 i18nc("@title:window", "Confirm Certificate Export"),
0187                 KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", 1)},
0188                 KStandardGuiItem::cancel(),
0189                 QStringLiteral("confirm-export-of-uncertified-keys"));
0190             return answer == KMessageBox::Continue;
0191         } else {
0192             std::sort(notCertifiedKeys.begin(), notCertifiedKeys.end());
0193             const auto answer = KMessageBox::warningContinueCancelList( //
0194                 parentWidgetOrView(),
0195                 xi18nc("@info",
0196                        "<para>You haven't certified all valid user IDs of the certificates listed below "
0197                        "with exportable certifications. People relying on your certifications "
0198                        "may not be able to verify the certificates.</para>"
0199                        "<para>Do you want to continue the export?</para>"),
0200                 notCertifiedKeys,
0201                 i18nc("@title:window", "Confirm Certificate Export"),
0202                 KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", pgpKeys.size())},
0203                 KStandardGuiItem::cancel(),
0204                 QStringLiteral("confirm-export-of-uncertified-keys"));
0205             return answer == KMessageBox::Continue;
0206         }
0207     }
0208 
0209     return true;
0210 }
0211 
0212 bool ExportCertificateCommand::Private::requestFileNames(GpgME::Protocol protocol)
0213 {
0214     if (protocol == UnknownProtocol) {
0215         if (!fileNames[GpgME::OpenPGP].isEmpty() && !fileNames[GpgME::CMS].isEmpty()) {
0216             return true;
0217         }
0218 
0219         /* Unknown protocol ask for first PGP Export file name */
0220         if (fileNames[GpgME::OpenPGP].isEmpty() && !requestFileNames(GpgME::OpenPGP)) {
0221             return false;
0222         }
0223         /* And then for CMS */
0224         return requestFileNames(GpgME::CMS);
0225     }
0226 
0227     if (!fileNames[protocol].isEmpty()) {
0228         return true;
0229     }
0230 
0231     const auto lastDir = ApplicationState::lastUsedExportDirectory();
0232 
0233     QString proposedFileName = lastDir + QLatin1Char('/');
0234     if (keys().size() == 1) {
0235         const bool usePGPFileExt = FileOperationsPreferences().usePGPFileExt();
0236         const auto key = keys().front();
0237         auto name = Formatting::prettyName(key);
0238         if (name.isEmpty()) {
0239             name = Formatting::prettyEMail(key);
0240         }
0241         const auto asciiArmoredCertificateClass = (protocol == OpenPGP ? Class::OpenPGP : Class::CMS) | Class::Ascii | Class::Certificate;
0242         /* Not translated so it's better to use in tutorials etc. */
0243         proposedFileName += QStringLiteral("%1_%2_public.%3")
0244                                 .arg(name)
0245                                 .arg(Formatting::prettyKeyID(key.shortKeyID()))
0246                                 .arg(outputFileExtension(asciiArmoredCertificateClass, usePGPFileExt));
0247     }
0248     if (protocol == GpgME::CMS) {
0249         if (!fileNames[GpgME::OpenPGP].isEmpty()) {
0250             /* If the user has already selected a PGP file name then use that as basis
0251              * for a proposal for the S/MIME file. */
0252             proposedFileName = fileNames[GpgME::OpenPGP];
0253             const int idx = proposedFileName.size() - 4;
0254             if (proposedFileName.endsWith(QLatin1StringView(".asc"))) {
0255                 proposedFileName.replace(idx, 4, QLatin1StringView(".pem"));
0256             }
0257             if (proposedFileName.endsWith(QLatin1StringView(".gpg")) || proposedFileName.endsWith(QLatin1String(".pgp"))) {
0258                 proposedFileName.replace(idx, 4, QLatin1StringView(".der"));
0259             }
0260         }
0261     }
0262 
0263     if (proposedFileName.isEmpty()) {
0264         proposedFileName = lastDir;
0265         proposedFileName += i18nc("A generic filename for exported certificates", "certificates");
0266         proposedFileName += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem");
0267     }
0268 
0269     auto fname = FileDialog::getSaveFileNameEx(parentWidgetOrView(),
0270                                                i18nc("1 is protocol", "Export %1 Certificates", Formatting::displayName(protocol)),
0271                                                QStringLiteral("imp"),
0272                                                proposedFileName,
0273                                                protocol == GpgME::OpenPGP ? i18n("OpenPGP Certificates") + QLatin1StringView(" (*.asc *.gpg *.pgp)")
0274                                                                           : i18n("S/MIME Certificates") + QLatin1StringView(" (*.pem *.der)"));
0275 
0276     if (!fname.isEmpty() && protocol == GpgME::CMS && fileNames[GpgME::OpenPGP] == fname) {
0277         KMessageBox::error(parentWidgetOrView(),
0278                            i18n("You have to select different filenames for different protocols."),
0279                            i18nc("@title:window", "Export Error"));
0280         return false;
0281     }
0282     const QFileInfo fi(fname);
0283     if (fi.suffix().isEmpty()) {
0284         fname += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem");
0285     }
0286 
0287     fileNames[protocol] = fname;
0288     ApplicationState::setLastUsedExportDirectory(fi.absolutePath());
0289     return !fname.isEmpty();
0290 }
0291 
0292 void ExportCertificateCommand::Private::startExportJob(GpgME::Protocol protocol, const std::vector<Key> &keys)
0293 {
0294     Q_ASSERT(protocol != GpgME::UnknownProtocol);
0295 
0296     const QGpgME::Protocol *const backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
0297     Q_ASSERT(backend);
0298     const QString fileName = fileNames[protocol];
0299     const bool binary = protocol == GpgME::OpenPGP
0300         ? fileName.endsWith(QLatin1StringView(".gpg"), Qt::CaseInsensitive) || fileName.endsWith(QLatin1String(".pgp"), Qt::CaseInsensitive)
0301         : fileName.endsWith(QLatin1StringView(".der"), Qt::CaseInsensitive);
0302     std::unique_ptr<ExportJob> job(backend->publicKeyExportJob(!binary));
0303     Q_ASSERT(job.get());
0304 
0305     connect(job.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &result, const QByteArray &keyData) {
0306         exportResult(result, keyData);
0307     });
0308 
0309     connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress);
0310 
0311     QStringList fingerprints;
0312     fingerprints.reserve(keys.size());
0313     for (const Key &i : keys) {
0314         fingerprints << QLatin1StringView(i.primaryFingerprint());
0315     }
0316 
0317     const GpgME::Error err = job->start(fingerprints);
0318     if (err) {
0319         showError(err);
0320         finished();
0321         return;
0322     }
0323     Q_EMIT q->info(i18n("Exporting certificates..."));
0324     ++jobsPending;
0325     const QPointer<ExportJob> exportJob(job.release());
0326 
0327     outFileForSender[exportJob.data()] = fileName;
0328     (protocol == CMS ? cmsJob : pgpJob) = exportJob;
0329 }
0330 
0331 void ExportCertificateCommand::Private::showError(const GpgME::Error &err)
0332 {
0333     Q_ASSERT(err);
0334     const QString msg = i18n(
0335         "<qt><p>An error occurred while trying to export "
0336         "the certificate:</p>"
0337         "<p><b>%1</b></p></qt>",
0338         Formatting::errorAsString(err));
0339     error(msg, i18n("Certificate Export Failed"));
0340 }
0341 
0342 void ExportCertificateCommand::doCancel()
0343 {
0344     d->cancelJobs();
0345 }
0346 
0347 void ExportCertificateCommand::Private::finishedIfLastJob()
0348 {
0349     if (jobsPending <= 0) {
0350         finished();
0351     }
0352 }
0353 
0354 static bool write_complete(QIODevice &iod, const QByteArray &data)
0355 {
0356     qint64 total = 0;
0357     qint64 toWrite = data.size();
0358     while (total < toWrite) {
0359         const qint64 written = iod.write(data.data() + total, toWrite);
0360         if (written < 0) {
0361             return false;
0362         }
0363         total += written;
0364         toWrite -= written;
0365     }
0366     return true;
0367 }
0368 
0369 void ExportCertificateCommand::Private::exportResult(const GpgME::Error &err, const QByteArray &data)
0370 {
0371     Q_ASSERT(jobsPending > 0);
0372     --jobsPending;
0373 
0374     Q_ASSERT(outFileForSender.contains(q->sender()));
0375     const QString outFile = outFileForSender[q->sender()];
0376 
0377     if (err) {
0378         showError(err);
0379         finishedIfLastJob();
0380         return;
0381     }
0382     QSaveFile savefile(outFile);
0383     // TODO: use KIO
0384     const QString writeErrorMsg = i18n("Could not write to file %1.", outFile);
0385     const QString errorCaption = i18n("Certificate Export Failed");
0386     if (!savefile.open(QIODevice::WriteOnly)) {
0387         error(writeErrorMsg, errorCaption);
0388         finishedIfLastJob();
0389         return;
0390     }
0391 
0392     if (!write_complete(savefile, data) || !savefile.commit()) {
0393         error(writeErrorMsg, errorCaption);
0394     }
0395     finishedIfLastJob();
0396 }
0397 
0398 void ExportCertificateCommand::Private::cancelJobs()
0399 {
0400     if (cmsJob) {
0401         cmsJob->slotCancel();
0402     }
0403     if (pgpJob) {
0404         pgpJob->slotCancel();
0405     }
0406 }
0407 
0408 #undef d
0409 #undef q
0410 
0411 #include "moc_exportcertificatecommand.cpp"