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

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     commands/exportsecretsubkeycommand.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2022 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 <config-kleopatra.h>
0012 
0013 #include "command_p.h"
0014 #include "exportsecretsubkeycommand.h"
0015 
0016 #include "fileoperationspreferences.h"
0017 #include <utils/applicationstate.h>
0018 #include <utils/filedialog.h>
0019 
0020 #include <Libkleo/Classify>
0021 #include <Libkleo/Formatting>
0022 
0023 #include <KLocalizedString>
0024 #include <KSharedConfig>
0025 
0026 #include <QGpgME/ExportJob>
0027 #include <QGpgME/Protocol>
0028 
0029 #include <QFileInfo>
0030 #include <QStandardPaths>
0031 
0032 #include <algorithm>
0033 #include <memory>
0034 #include <vector>
0035 
0036 using namespace Kleo;
0037 using namespace GpgME;
0038 
0039 namespace
0040 {
0041 
0042 QString openPGPCertificateFileExtension()
0043 {
0044     return outputFileExtension(Class::OpenPGP | Class::Ascii | Class::Certificate, FileOperationsPreferences().usePGPFileExt());
0045 }
0046 
0047 QString proposeFilename(const std::vector<Subkey> &subkeys)
0048 {
0049     QString filename;
0050 
0051     if (subkeys.size() == 1) {
0052         const auto subkey = subkeys.front();
0053         const auto key = subkey.parent();
0054         auto name = Formatting::prettyName(key);
0055         if (name.isEmpty()) {
0056             name = Formatting::prettyEMail(key);
0057         }
0058         const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID());
0059         const auto shortSubkeyID = Formatting::prettyKeyID(QByteArray{subkey.keyID()}.right(8).constData());
0060         const auto usage = Formatting::usageString(subkey).replace(QLatin1StringView{", "}, QLatin1String{"_"});
0061         /* Not translated so it's better to use in tutorials etc. */
0062         filename = QStringView{u"%1_%2_SECRET_SUBKEY_%3_%4"}.arg(name, shortKeyID, shortSubkeyID, usage);
0063     } else {
0064         filename = i18nc("Generic filename for exported subkeys", "subkeys");
0065     }
0066     filename.replace(u'/', u'_');
0067 
0068     return ApplicationState::lastUsedExportDirectory() + u'/' + filename + u'.' + openPGPCertificateFileExtension();
0069 }
0070 
0071 QString requestFilename(const std::vector<Subkey> &subkeys, const QString &proposedFilename, QWidget *parent)
0072 {
0073     auto filename = FileDialog::getSaveFileNameEx(parent,
0074                                                   i18ncp("@title:window", "Export Subkey", "Export Subkeys", subkeys.size()),
0075                                                   QStringLiteral("imp"),
0076                                                   proposedFilename,
0077                                                   i18nc("description of filename filter", "Secret Key Files") + QLatin1StringView{" (*.asc *.gpg *.pgp)"});
0078 
0079     if (!filename.isEmpty()) {
0080         const QFileInfo fi{filename};
0081         if (fi.suffix().isEmpty()) {
0082             filename += u'.' + openPGPCertificateFileExtension();
0083         }
0084         ApplicationState::setLastUsedExportDirectory(filename);
0085     }
0086 
0087     return filename;
0088 }
0089 
0090 template<typename SubkeyContainer>
0091 QStringList getSubkeyFingerprints(const SubkeyContainer &subkeys)
0092 {
0093     QStringList fingerprints;
0094 
0095     fingerprints.reserve(subkeys.size());
0096     std::transform(std::begin(subkeys), std::end(subkeys), std::back_inserter(fingerprints), [](const auto &subkey) {
0097         return QLatin1StringView{subkey.fingerprint()} + u'!';
0098     });
0099 
0100     return fingerprints;
0101 }
0102 }
0103 
0104 class ExportSecretSubkeyCommand::Private : public Command::Private
0105 {
0106     friend class ::ExportSecretSubkeyCommand;
0107     ExportSecretSubkeyCommand *q_func() const
0108     {
0109         return static_cast<ExportSecretSubkeyCommand *>(q);
0110     }
0111 
0112 public:
0113     explicit Private(ExportSecretSubkeyCommand *qq);
0114     ~Private() override;
0115 
0116     void start();
0117     void cancel();
0118 
0119 private:
0120     std::unique_ptr<QGpgME::ExportJob> startExportJob(const std::vector<Subkey> &subkeys);
0121     void onExportJobResult(const Error &err, const QByteArray &keyData);
0122     void showError(const Error &err);
0123 
0124 private:
0125     std::vector<Subkey> subkeys;
0126     QString filename;
0127     QPointer<QGpgME::ExportJob> job;
0128 };
0129 
0130 ExportSecretSubkeyCommand::Private *ExportSecretSubkeyCommand::d_func()
0131 {
0132     return static_cast<Private *>(d.get());
0133 }
0134 const ExportSecretSubkeyCommand::Private *ExportSecretSubkeyCommand::d_func() const
0135 {
0136     return static_cast<const Private *>(d.get());
0137 }
0138 
0139 #define d d_func()
0140 #define q q_func()
0141 
0142 ExportSecretSubkeyCommand::Private::Private(ExportSecretSubkeyCommand *qq)
0143     : Command::Private{qq}
0144 {
0145 }
0146 
0147 ExportSecretSubkeyCommand::Private::~Private() = default;
0148 
0149 void ExportSecretSubkeyCommand::Private::start()
0150 {
0151     if (subkeys.empty()) {
0152         finished();
0153         return;
0154     }
0155 
0156     filename = requestFilename(subkeys, proposeFilename(subkeys), parentWidgetOrView());
0157     if (filename.isEmpty()) {
0158         canceled();
0159         return;
0160     }
0161 
0162     auto exportJob = startExportJob(subkeys);
0163     if (!exportJob) {
0164         finished();
0165         return;
0166     }
0167     job = exportJob.release();
0168 }
0169 
0170 void ExportSecretSubkeyCommand::Private::cancel()
0171 {
0172     if (job) {
0173         job->slotCancel();
0174     }
0175     job.clear();
0176 }
0177 
0178 std::unique_ptr<QGpgME::ExportJob> ExportSecretSubkeyCommand::Private::startExportJob(const std::vector<Subkey> &subkeys)
0179 {
0180     const bool armor = filename.endsWith(u".asc", Qt::CaseInsensitive);
0181     std::unique_ptr<QGpgME::ExportJob> exportJob{QGpgME::openpgp()->secretSubkeyExportJob(armor)};
0182     Q_ASSERT(exportJob);
0183 
0184     connect(exportJob.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &err, const QByteArray &keyData) {
0185         onExportJobResult(err, keyData);
0186     });
0187     connect(exportJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress);
0188 
0189     const GpgME::Error err = exportJob->start(getSubkeyFingerprints(subkeys));
0190     if (err) {
0191         showError(err);
0192         return {};
0193     }
0194     Q_EMIT q->info(i18nc("@info:status", "Exporting subkeys..."));
0195 
0196     return exportJob;
0197 }
0198 
0199 void ExportSecretSubkeyCommand::Private::onExportJobResult(const Error &err, const QByteArray &keyData)
0200 {
0201     if (err) {
0202         showError(err);
0203         finished();
0204         return;
0205     }
0206 
0207     if (err.isCanceled()) {
0208         finished();
0209         return;
0210     }
0211 
0212     if (keyData.isEmpty()) {
0213         error(i18nc("@info", "The result of the export is empty."), i18nc("@title:window", "Export Failed"));
0214         finished();
0215         return;
0216     }
0217 
0218     QFile f{filename};
0219     if (!f.open(QIODevice::WriteOnly)) {
0220         error(xi18nc("@info", "Cannot open file <filename>%1</filename> for writing.", filename), i18nc("@title:window", "Export Failed"));
0221         finished();
0222         return;
0223     }
0224 
0225     const auto bytesWritten = f.write(keyData);
0226     if (bytesWritten != keyData.size()) {
0227         error(xi18ncp("@info",
0228                       "Writing subkey to file <filename>%2</filename> failed.",
0229                       "Writing subkeys to file <filename>%2</filename> failed.",
0230                       subkeys.size(),
0231                       filename),
0232               i18nc("@title:window", "Export Failed"));
0233         finished();
0234         return;
0235     }
0236 
0237     information(i18ncp("@info", "The subkey was exported successfully.", "%1 subkeys were exported successfully.", subkeys.size()),
0238                 i18nc("@title:window", "Secret Key Backup"));
0239     finished();
0240 }
0241 
0242 void ExportSecretSubkeyCommand::Private::showError(const Error &err)
0243 {
0244     error(xi18nc("@info",
0245                  "<para>An error occurred during the export:</para>"
0246                  "<para><message>%1</message></para>",
0247                  Formatting::errorAsString(err)),
0248           i18nc("@title:window", "Export Failed"));
0249 }
0250 
0251 ExportSecretSubkeyCommand::ExportSecretSubkeyCommand(const std::vector<GpgME::Subkey> &subkeys)
0252     : Command{new Private{this}}
0253 {
0254     d->subkeys = subkeys;
0255 }
0256 
0257 ExportSecretSubkeyCommand::~ExportSecretSubkeyCommand() = default;
0258 
0259 void ExportSecretSubkeyCommand::doStart()
0260 {
0261     d->start();
0262 }
0263 
0264 void ExportSecretSubkeyCommand::doCancel()
0265 {
0266     d->cancel();
0267 }
0268 
0269 #undef d
0270 #undef q
0271 
0272 #include "moc_exportsecretsubkeycommand.cpp"