Commit 0c8a90d4 authored by Ingo Klöcker's avatar Ingo Klöcker
Browse files

Add export of secret subkeys

This adds support for exporting secret subkeys of OpenPGP keys. (X.509
certificates have no subkeys.) The export can be triggered from the
"Subkeys Details" dialog.

FEATURE:
GnuPG-bug-id: 5755
parent d88da8ea
Pipeline #119731 passed with stage
in 1 minute and 44 seconds
......@@ -112,6 +112,7 @@ if (QGpgme_VERSION VERSION_GREATER_EQUAL "1.16.1")
set(QGPGME_SUPPORTS_WKDLOOKUP 1)
set(QGPGME_SUPPORTS_IMPORT_WITH_FILTER 1)
set(QGPGME_SUPPORTS_IMPORT_WITH_KEY_ORIGIN 1)
set(QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT 1)
endif()
# Kdepimlibs packages
......
......@@ -47,3 +47,6 @@
/* Defined if QGpgME supports setting key origin when importing keys */
#cmakedefine QGPGME_SUPPORTS_IMPORT_WITH_KEY_ORIGIN 1
/* Defined if QGpgME supports the export of secret subkeys */
#cmakedefine QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT 1
......@@ -237,6 +237,7 @@ set(_kleopatra_SRCS
commands/revokecertificationcommand.cpp
commands/selftestcommand.cpp
commands/exportsecretkeycommand.cpp
commands/exportsecretsubkeycommand.cpp
commands/exportopenpgpcertstoservercommand.cpp
commands/adduseridcommand.cpp
commands/newcertificatecommand.cpp
......
/* -*- mode: c++; c-basic-offset:4 -*-
commands/exportsecretsubkeycommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config-kleopatra.h>
#include "exportsecretsubkeycommand.h"
#include "command_p.h"
#include "fileoperationspreferences.h"
#include "utils/filedialog.h"
#include <Libkleo/Classify>
#include <Libkleo/Formatting>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <QGpgME/Protocol>
#include <QGpgME/ExportJob>
#include <QFileInfo>
#include <QStandardPaths>
#include <algorithm>
#include <memory>
#include <vector>
using namespace Kleo;
using namespace GpgME;
namespace
{
#ifdef QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT
QString getLastUsedExportDirectory()
{
KConfigGroup config{KSharedConfig::openConfig(), "ExportDialog"};
return config.readEntry("LastDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
}
void updateLastUsedExportDirectory(const QString &path)
{
KConfigGroup config{KSharedConfig::openConfig(), "ExportDialog"};
config.writeEntry("LastDirectory", QFileInfo{path}.absolutePath());
}
QString openPGPCertificateFileExtension()
{
return QLatin1String{outputFileExtension(Class::OpenPGP | Class::Ascii | Class::Certificate,
FileOperationsPreferences().usePGPFileExt())};
}
QString proposeFilename(const std::vector<Subkey> &subkeys)
{
QString filename;
if (subkeys.size() == 1) {
const auto subkey = subkeys.front();
const auto key = subkey.parent();
auto name = Formatting::prettyName(key);
if (name.isEmpty()) {
name = Formatting::prettyEMail(key);
}
const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID());
const auto shortSubkeyID = Formatting::prettyKeyID(QByteArray{subkey.keyID()}.right(8).constData());
const auto usage = Formatting::usageString(subkey).replace(QLatin1String{", "}, QLatin1String{"_"});
/* Not translated so it's better to use in tutorials etc. */
filename += QStringView{u"%1_%2_SECRET_SUBKEY_%3_%4"}.arg(
name, shortKeyID, shortSubkeyID, usage);
} else {
filename += i18nc("Generic filename for exported subkeys", "subkeys");
}
filename.replace(u'/', u'_');
return getLastUsedExportDirectory() + u'/' + filename + u'.' + openPGPCertificateFileExtension();
}
QString requestFilename(const std::vector<Subkey> &subkeys, const QString &proposedFilename, QWidget *parent)
{
auto filename = FileDialog::getSaveFileNameEx(
parent,
i18ncp("@title:window", "Export Subkey", "Export Subkeys", subkeys.size()),
QStringLiteral("imp"),
proposedFilename,
i18nc("description of filename filter", "Secret Key Files") + QLatin1String{" (*.asc *.gpg *.pgp)"});
if (!filename.isEmpty()) {
const QFileInfo fi{filename};
if (fi.suffix().isEmpty()) {
filename += u'.' + openPGPCertificateFileExtension();
}
updateLastUsedExportDirectory(filename);
}
return filename;
}
template<typename SubkeyContainer>
QStringList getSubkeyFingerprints(const SubkeyContainer &subkeys)
{
QStringList fingerprints;
fingerprints.reserve(subkeys.size());
std::transform(std::begin(subkeys), std::end(subkeys),
std::back_inserter(fingerprints),
[](const auto &subkey) {
return QLatin1String{subkey.fingerprint()} + u'!';
});
return fingerprints;
}
#endif
}
class ExportSecretSubkeyCommand::Private : public Command::Private
{
friend class ::ExportSecretSubkeyCommand;
ExportSecretSubkeyCommand *q_func() const
{
return static_cast<ExportSecretSubkeyCommand *>(q);
}
public:
explicit Private(ExportSecretSubkeyCommand *qq);
~Private() override;
void start();
void cancel();
private:
std::unique_ptr<QGpgME::ExportJob> startExportJob(const std::vector<Subkey> &subkeys);
void onExportJobResult(const Error &err, const QByteArray &keyData);
void showError(const Error &err);
private:
std::vector<Subkey> subkeys;
QString filename;
QPointer<QGpgME::ExportJob> job;
};
ExportSecretSubkeyCommand::Private *ExportSecretSubkeyCommand::d_func()
{
return static_cast<Private *>(d.get());
}
const ExportSecretSubkeyCommand::Private *ExportSecretSubkeyCommand::d_func() const
{
return static_cast<const Private *>(d.get());
}
#define d d_func()
#define q q_func()
ExportSecretSubkeyCommand::Private::Private(ExportSecretSubkeyCommand *qq)
: Command::Private{qq}
{
}
ExportSecretSubkeyCommand::Private::~Private() = default;
void ExportSecretSubkeyCommand::Private::start()
{
#ifdef QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT
if (subkeys.empty()) {
finished();
return;
}
filename = requestFilename(subkeys, proposeFilename(subkeys), parentWidgetOrView());
if (filename.isEmpty()) {
canceled();
return;
}
auto exportJob = startExportJob(subkeys);
if (!exportJob) {
finished();
return;
}
job = exportJob.release();
#else
Q_ASSERT(!"This command is not supported by the backend it was compiled against");
finished();
return;
#endif
}
void ExportSecretSubkeyCommand::Private::cancel()
{
if (job) {
job->slotCancel();
}
job.clear();
}
std::unique_ptr<QGpgME::ExportJob> ExportSecretSubkeyCommand::Private::startExportJob(const std::vector<Subkey> &subkeys)
{
#ifdef QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT
const bool armor = filename.endsWith(u".asc", Qt::CaseInsensitive);
std::unique_ptr<QGpgME::ExportJob> exportJob{QGpgME::openpgp()->secretSubkeyExportJob(armor)};
Q_ASSERT(exportJob);
connect(exportJob.get(), &QGpgME::ExportJob::result,
q, [this](const GpgME::Error &err, const QByteArray &keyData) {
onExportJobResult(err, keyData);
});
connect(exportJob.get(), &QGpgME::Job::progress,
q, &Command::progress);
const GpgME::Error err = exportJob->start(getSubkeyFingerprints(subkeys));
if (err) {
showError(err);
return {};
}
Q_EMIT q->info(i18nc("@info:status", "Exporting subkeys..."));
return exportJob;
#else
Q_UNUSED(subkeys)
return {};
#endif
}
void ExportSecretSubkeyCommand::Private::onExportJobResult(const Error &err, const QByteArray &keyData)
{
if (err) {
showError(err);
finished();
return;
}
if (keyData.isEmpty()) {
error(i18nc("@info", "The result of the export is empty."),
i18nc("@title:window", "Export Failed"));
finished();
return;
}
QFile f{filename};
if (!f.open(QIODevice::WriteOnly)) {
error(xi18nc("@info", "Cannot open file <filename>%1</filename> for writing.", filename),
i18nc("@title:window", "Export Failed"));
finished();
return;
}
const auto bytesWritten = f.write(keyData);
if (bytesWritten != keyData.size()) {
error(xi18ncp("@info",
"Writing subkey to file <filename>%1</filename> failed.",
"Writing subkeys to file <filename>%1</filename> failed.",
subkeys.size(), filename),
i18nc("@title:window", "Export Failed"));
}
finished();
}
void ExportSecretSubkeyCommand::Private::showError(const Error &err)
{
error(xi18nc("@info",
"<para>An error occurred during the export:</para>"
"<para><message>%1</message></para>",
QString::fromLocal8Bit(err.asString())),
i18nc("@title:window", "Export Failed"));
}
ExportSecretSubkeyCommand::ExportSecretSubkeyCommand(const std::vector<GpgME::Subkey> &subkeys)
: Command{new Private{this}}
{
d->subkeys = subkeys;
}
ExportSecretSubkeyCommand::~ExportSecretSubkeyCommand() = default;
void ExportSecretSubkeyCommand::doStart()
{
d->start();
}
void ExportSecretSubkeyCommand::doCancel()
{
d->cancel();
}
#undef d
#undef q
#include "moc_exportsecretsubkeycommand.cpp"
/* -*- mode: c++; c-basic-offset:4 -*-
commands/exportsecretsubkeycommand.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "command.h"
namespace GpgME
{
class Subkey;
}
namespace Kleo
{
class ExportSecretSubkeyCommand : public Command
{
Q_OBJECT
public:
explicit ExportSecretSubkeyCommand(const std::vector<GpgME::Subkey> &subkeys);
~ExportSecretSubkeyCommand() override;
private:
void doStart() override;
void doCancel() override;
private:
class Private;
inline Private *d_func();
inline const Private *d_func() const;
};
}
/* SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB
/*
dialogs/subkeyswidget.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB
SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik
SPDX-FileContributor: Intevation GmbH
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config-kleopatra.h>
#include "subkeyswidget.h"
#include "ui_subkeyswidget.h"
#include "commands/changeexpirycommand.h"
#ifdef QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT
#include "commands/exportsecretsubkeycommand.h"
#endif
#include "commands/keytocardcommand.h"
#include "commands/importpaperkeycommand.h"
#include "exportdialog.h"
......@@ -57,13 +67,14 @@ void SubKeysWidget::Private::tableContextMenuRequested(const QPoint &p)
return;
}
const auto subkey = item->data(0, Qt::UserRole).value<GpgME::Subkey>();
const bool isOpenPGPKey = subkey.parent().protocol() == GpgME::OpenPGP;
auto menu = new QMenu(q);
connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater);
bool hasActions = false;
if (subkey.parent().protocol() == GpgME::OpenPGP && subkey.parent().hasSecret()) {
if (isOpenPGPKey && subkey.parent().hasSecret()) {
hasActions = true;
menu->addAction(i18n("Change Expiry Date..."), q,
[this, subkey]() {
......@@ -82,7 +93,7 @@ void SubKeysWidget::Private::tableContextMenuRequested(const QPoint &p)
);
}
if (subkey.parent().protocol() == GpgME::OpenPGP && subkey.canAuthenticate()) {
if (isOpenPGPKey && subkey.canAuthenticate()) {
hasActions = true;
menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")),
i18n("Export OpenSSH key"),
......@@ -122,6 +133,23 @@ void SubKeysWidget::Private::tableContextMenuRequested(const QPoint &p)
action->setEnabled(!KeyToCardCommand::getSuitableCards(subkey).empty());
}
#ifdef QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT
const bool isPrimarySubkey = subkey.keyID() == key.keyID();
if (isOpenPGPKey && subkey.isSecret() && !isPrimarySubkey) {
hasActions = true;
menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")),
i18n("Export secret subkey"),
q, [this, subkey]() {
auto cmd = new ExportSecretSubkeyCommand{{subkey}};
ui.subkeysTree->setEnabled(false);
connect(cmd, &ExportSecretSubkeyCommand::finished,
q, [this]() { ui.subkeysTree->setEnabled(true); });
cmd->setParentWidget(q);
cmd->start();
});
}
#endif
if (hasActions) {
menu->popup(ui.subkeysTree->viewport()->mapToGlobal(p));
} else {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment