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

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     commands/exportsecretkeycommand.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 "exportsecretkeycommand.h"
0015 
0016 #include "fileoperationspreferences.h"
0017 #include "utils/filedialog.h"
0018 #include <utils/applicationstate.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 <gpgme++/context.h>
0033 
0034 #include <algorithm>
0035 #include <memory>
0036 #include <vector>
0037 
0038 #include <kleopatra_debug.h>
0039 
0040 using namespace Kleo;
0041 using namespace Kleo::Commands;
0042 using namespace GpgME;
0043 
0044 namespace
0045 {
0046 
0047 QString openPGPCertificateFileExtension()
0048 {
0049     return outputFileExtension(Class::OpenPGP | Class::Ascii | Class::Certificate, FileOperationsPreferences().usePGPFileExt());
0050 }
0051 
0052 QString cmsCertificateFileExtension()
0053 {
0054     return outputFileExtension(Class::CMS | Class::Binary | Class::ExportedPSM,
0055                                /*usePGPFileExt=*/false);
0056 }
0057 
0058 QString certificateFileExtension(GpgME::Protocol protocol)
0059 {
0060     switch (protocol) {
0061     case GpgME::OpenPGP:
0062         return openPGPCertificateFileExtension();
0063     case GpgME::CMS:
0064         return cmsCertificateFileExtension();
0065     default:
0066         qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Unknown protocol" << protocol;
0067         return QStringLiteral("txt");
0068     }
0069 }
0070 
0071 QString proposeFilename(const Key &key)
0072 {
0073     QString filename;
0074 
0075     auto name = Formatting::prettyName(key);
0076     if (name.isEmpty()) {
0077         name = Formatting::prettyEMail(key);
0078     }
0079     const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID());
0080     /* Not translated so it's better to use in tutorials etc. */
0081     filename = QStringView{u"%1_%2_SECRET"}.arg(name, shortKeyID);
0082     filename.replace(u'/', u'_');
0083 
0084     return ApplicationState::lastUsedExportDirectory() + u'/' + filename + u'.' + certificateFileExtension(key.protocol());
0085 }
0086 
0087 QString secretKeyFileFilters(GpgME::Protocol protocol)
0088 {
0089     switch (protocol) {
0090     case GpgME::OpenPGP:
0091         return i18nc("description of filename filter", "Secret Key Files") + QLatin1StringView{" (*.asc *.gpg *.pgp)"};
0092     case GpgME::CMS:
0093         return i18nc("description of filename filter", "Secret Key Files") + QLatin1StringView{" (*.p12)"};
0094     default:
0095         qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Unknown protocol" << protocol;
0096         return i18nc("description of filename filter", "All Files") + QLatin1StringView{" (*)"};
0097     }
0098 }
0099 
0100 QString requestFilename(const Key &key, const QString &proposedFilename, QWidget *parent)
0101 {
0102     auto filename = FileDialog::getSaveFileNameEx(parent,
0103                                                   i18nc("@title:window", "Secret Key Backup"),
0104                                                   QStringLiteral("imp"),
0105                                                   proposedFilename,
0106                                                   secretKeyFileFilters(key.protocol()));
0107 
0108     if (!filename.isEmpty()) {
0109         const QFileInfo fi{filename};
0110         if (fi.suffix().isEmpty()) {
0111             filename += u'.' + certificateFileExtension(key.protocol());
0112         }
0113         ApplicationState::setLastUsedExportDirectory(filename);
0114     }
0115 
0116     return filename;
0117 }
0118 
0119 QString errorCaption()
0120 {
0121     return i18nc("@title:window", "Secret Key Backup Error");
0122 }
0123 
0124 }
0125 
0126 class ExportSecretKeyCommand::Private : public Command::Private
0127 {
0128     friend class ::ExportSecretKeyCommand;
0129     ExportSecretKeyCommand *q_func() const
0130     {
0131         return static_cast<ExportSecretKeyCommand *>(q);
0132     }
0133 
0134 public:
0135     explicit Private(ExportSecretKeyCommand *qq, KeyListController *c = nullptr);
0136     ~Private() override;
0137 
0138     void start();
0139     void cancel();
0140 
0141 private:
0142     std::unique_ptr<QGpgME::ExportJob> startExportJob(const Key &key);
0143     void onExportJobResult(const Error &err, const QByteArray &keyData);
0144     void showError(const Error &err);
0145 
0146 private:
0147     QString filename;
0148     QPointer<QGpgME::ExportJob> job;
0149 };
0150 
0151 ExportSecretKeyCommand::Private *ExportSecretKeyCommand::d_func()
0152 {
0153     return static_cast<Private *>(d.get());
0154 }
0155 const ExportSecretKeyCommand::Private *ExportSecretKeyCommand::d_func() const
0156 {
0157     return static_cast<const Private *>(d.get());
0158 }
0159 
0160 #define d d_func()
0161 #define q q_func()
0162 
0163 ExportSecretKeyCommand::Private::Private(ExportSecretKeyCommand *qq, KeyListController *c)
0164     : Command::Private{qq, c}
0165 {
0166 }
0167 
0168 ExportSecretKeyCommand::Private::~Private() = default;
0169 
0170 void ExportSecretKeyCommand::Private::start()
0171 {
0172     const Key key = this->key();
0173 
0174     if (key.isNull()) {
0175         finished();
0176         return;
0177     }
0178 
0179     filename = requestFilename(key, proposeFilename(key), parentWidgetOrView());
0180     if (filename.isEmpty()) {
0181         canceled();
0182         return;
0183     }
0184 
0185     auto exportJob = startExportJob(key);
0186     if (!exportJob) {
0187         finished();
0188         return;
0189     }
0190     job = exportJob.release();
0191 }
0192 
0193 void ExportSecretKeyCommand::Private::cancel()
0194 {
0195     if (job) {
0196         job->slotCancel();
0197     }
0198     job.clear();
0199 }
0200 
0201 std::unique_ptr<QGpgME::ExportJob> ExportSecretKeyCommand::Private::startExportJob(const Key &key)
0202 {
0203     const bool armor = key.protocol() == GpgME::OpenPGP && filename.endsWith(u".asc", Qt::CaseInsensitive);
0204     const QGpgME::Protocol *const backend = (key.protocol() == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
0205     Q_ASSERT(backend);
0206     std::unique_ptr<QGpgME::ExportJob> exportJob{backend->secretKeyExportJob(armor)};
0207     Q_ASSERT(exportJob);
0208 
0209     if (key.protocol() == GpgME::CMS) {
0210         exportJob->setExportFlags(GpgME::Context::ExportPKCS12);
0211     }
0212 
0213     connect(exportJob.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &err, const QByteArray &keyData) {
0214         onExportJobResult(err, keyData);
0215     });
0216     connect(exportJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress);
0217 
0218     const GpgME::Error err = exportJob->start({QLatin1StringView{key.primaryFingerprint()}});
0219     if (err) {
0220         showError(err);
0221         return {};
0222     }
0223     Q_EMIT q->info(i18nc("@info:status", "Backing up secret key..."));
0224 
0225     return exportJob;
0226 }
0227 
0228 void ExportSecretKeyCommand::Private::onExportJobResult(const Error &err, const QByteArray &keyData)
0229 {
0230     if (err.isCanceled()) {
0231         finished();
0232         return;
0233     }
0234 
0235     if (err) {
0236         showError(err);
0237         finished();
0238         return;
0239     }
0240 
0241     if (keyData.isEmpty()) {
0242         error(i18nc("@info", "The result of the backup is empty. Maybe you entered an empty or a wrong passphrase."), errorCaption());
0243         finished();
0244         return;
0245     }
0246 
0247     QFile f{filename};
0248     if (!f.open(QIODevice::WriteOnly)) {
0249         error(xi18nc("@info", "Cannot open file <filename>%1</filename> for writing.", filename), errorCaption());
0250         finished();
0251         return;
0252     }
0253 
0254     const auto bytesWritten = f.write(keyData);
0255     if (bytesWritten != keyData.size()) {
0256         error(xi18nc("@info", "Writing key to file <filename>%1</filename> failed.", filename), errorCaption());
0257         finished();
0258         return;
0259     }
0260 
0261     information(i18nc("@info", "The backup of the secret key was created successfully."), i18nc("@title:window", "Secret Key Backup"));
0262     finished();
0263 }
0264 
0265 void ExportSecretKeyCommand::Private::showError(const Error &err)
0266 {
0267     error(xi18nc("@info",
0268                  "<para>An error occurred during the backup of the secret key:</para>"
0269                  "<para><message>%1</message></para>",
0270                  Formatting::errorAsString(err)),
0271           errorCaption());
0272 }
0273 
0274 ExportSecretKeyCommand::ExportSecretKeyCommand(QAbstractItemView *view, KeyListController *controller)
0275     : Command{view, new Private{this, controller}}
0276 {
0277 }
0278 
0279 ExportSecretKeyCommand::ExportSecretKeyCommand(const GpgME::Key &key)
0280     : Command{key, new Private{this}}
0281 {
0282 }
0283 
0284 ExportSecretKeyCommand::~ExportSecretKeyCommand() = default;
0285 
0286 void ExportSecretKeyCommand::doStart()
0287 {
0288     d->start();
0289 }
0290 
0291 void ExportSecretKeyCommand::doCancel()
0292 {
0293     d->cancel();
0294 }
0295 
0296 #undef d
0297 #undef q
0298 
0299 #include "moc_exportsecretkeycommand.cpp"