Commit 5c4d3a80 authored by Ingo Klöcker's avatar Ingo Klöcker
Browse files

Allow the export of certificate groups

Certificate groups are exported together with the public key data of
all OpenPGP keys and all X.509 certificates that are part of the group.
Currently, groups can only be exported one by one.

GnuPG-bug-id: 5638
parent 36e12fc6
......@@ -214,6 +214,7 @@ set(_kleopatra_SRCS
commands/gnupgprocesscommand.cpp
commands/detailscommand.cpp
commands/exportcertificatecommand.cpp
commands/exportgroupscommand.cpp
commands/importcertificatescommand.cpp
commands/importcertificatefromfilecommand.cpp
commands/importcertificatefromclipboardcommand.cpp
......
/* -*- mode: c++; c-basic-offset:4 -*-
exportgroupscommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2021 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 "exportgroupscommand.h"
#include "command_p.h"
#include "utils/filedialog.h"
#include <Libkleo/Algorithm>
#include <Libkleo/KeyGroup>
#include <Libkleo/KeyGroupConfig>
#include <Libkleo/KeyHelpers>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <QGpgME/Protocol>
#include <QGpgME/ExportJob>
#include <QFileInfo>
#include <QStandardPaths>
#include <memory>
#include <vector>
using namespace Kleo;
using namespace GpgME;
using namespace QGpgME;
namespace
{
static const QString certificateGroupFileExtension{QLatin1String{".kgrp"}};
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 proposeFilename(const std::vector<KeyGroup> &groups)
{
QString filename;
filename = getLastUsedExportDirectory() + QLatin1Char{'/'};
if (groups.size() == 1) {
filename += groups.front().name().replace(QLatin1Char{'/'}, QLatin1Char{'_'});
} else {
filename += i18nc("A generic filename for exported certificate groups", "certificate groups");
}
return filename + certificateGroupFileExtension;
}
QString requestFilename(QWidget *parent, const std::vector<KeyGroup> &groups)
{
const QString proposedFilename = proposeFilename(groups);
auto filename = FileDialog::getSaveFileNameEx(
parent,
i18ncp("@title:window", "Export Certificate Group", "Export Certificate Groups", groups.size()),
QStringLiteral("imp"),
proposedFilename,
i18nc("filename filter like Certificate Groups (*.foo)", "Certificate Groups (*%1)", certificateGroupFileExtension));
if (!filename.isEmpty()) {
const QFileInfo fi{filename};
if (fi.suffix().isEmpty()) {
filename += certificateGroupFileExtension;
}
updateLastUsedExportDirectory(filename);
}
return filename;
}
}
class ExportGroupsCommand::Private : public Command::Private
{
friend class ::ExportGroupsCommand;
ExportGroupsCommand *q_func() const
{
return static_cast<ExportGroupsCommand *>(q);
}
public:
explicit Private(ExportGroupsCommand *qq);
~Private() override;
void start();
void exportGroups();
bool startExportJob(GpgME::Protocol protocol, const std::vector<Key> &keys);
void onExportJobResult(const QGpgME::Job *job, const GpgME::Error &err, const QByteArray &keyData);
void cancelJobs();
void showError(const GpgME::Error &err);
void finishedIfLastJob();
private:
std::vector<KeyGroup> groups;
QString filename;
std::vector<QPointer<QGpgME::Job>> exportJobs;
};
ExportGroupsCommand::Private *ExportGroupsCommand::d_func()
{
return static_cast<Private *>(d.get());
}
const ExportGroupsCommand::Private *ExportGroupsCommand::d_func() const
{
return static_cast<const Private *>(d.get());
}
#define d d_func()
#define q q_func()
ExportGroupsCommand::Private::Private(ExportGroupsCommand *qq)
: Command::Private(qq)
{
}
ExportGroupsCommand::Private::~Private() = default;
void ExportGroupsCommand::Private::start()
{
if (groups.empty()) {
finished();
return;
}
filename = requestFilename(parentWidgetOrView(), groups);
if (filename.isEmpty()) {
canceled();
return;
}
const auto groupKeys = std::accumulate(std::begin(groups), std::end(groups),
KeyGroup::Keys{},
[](auto &allKeys, const auto &group) {
const auto keys = group.keys();
allKeys.insert(std::begin(keys), std::end(keys));
return allKeys;
});
std::vector<Key> openpgpKeys;
std::vector<Key> cmsKeys;
std::partition_copy(std::begin(groupKeys), std::end(groupKeys),
std::back_inserter(openpgpKeys),
std::back_inserter(cmsKeys),
[](const GpgME::Key &key) {
return key.protocol() == GpgME::OpenPGP;
});
// remove/overwrite existing file
if (QFile::exists(filename) && !QFile::remove(filename)) {
error(xi18n("Cannot overwrite existing <filename>%1</filename>.", filename),
i18nc("@title:window", "Export Failed"));
finished();
return;
}
exportGroups();
if (!openpgpKeys.empty()) {
if (!startExportJob(GpgME::OpenPGP, openpgpKeys)) {
finished();
return;
}
}
if (!cmsKeys.empty()) {
if (!startExportJob(GpgME::CMS, cmsKeys)) {
finishedIfLastJob();
}
}
}
void ExportGroupsCommand::Private::exportGroups()
{
KeyGroupConfig config{filename};
config.writeGroups(groups);
}
bool ExportGroupsCommand::Private::startExportJob(GpgME::Protocol protocol, const std::vector<Key> &keys)
{
const QGpgME::Protocol *const backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
Q_ASSERT(backend);
std::unique_ptr<ExportJob> jobOwner(backend->publicKeyExportJob(/*armor=*/ true));
auto job = jobOwner.get();
Q_ASSERT(job);
connect(job, &ExportJob::result,
q, [this, job](const GpgME::Error &err, const QByteArray &keyData) {
onExportJobResult(job, err, keyData);
});
connect(job, &Job::progress,
q, &Command::progress);
const GpgME::Error err = job->start(Kleo::getFingerprints(keys));
if (err) {
showError(err);
return false;
}
Q_EMIT q->info(i18n("Exporting certificate groups..."));
exportJobs.push_back(jobOwner.release());
return true;
}
void ExportGroupsCommand::Private::onExportJobResult(const QGpgME::Job *job, const GpgME::Error &err, const QByteArray &keyData)
{
Q_ASSERT(Kleo::contains(exportJobs, job));
exportJobs.erase(std::remove(exportJobs.begin(), exportJobs.end(), job), exportJobs.end());
if (err) {
showError(err);
finishedIfLastJob();
return;
}
QFile f{filename};
if (!f.open(QIODevice::WriteOnly | QIODevice::Append)) {
error(xi18n("Cannot open file <filename>%1</filename> for writing.", filename),
i18nc("@title:window", "Export Failed"));
finishedIfLastJob();
return;
}
const auto bytesWritten = f.write(keyData);
if (bytesWritten != keyData.size()) {
error(xi18n("Writing certificates to file <filename>%1</filename> failed.", filename),
i18nc("@title:window", "Export Failed"));
}
finishedIfLastJob();
}
void ExportGroupsCommand::Private::showError(const GpgME::Error &err)
{
error(xi18n("<para>An error occurred during the export:</para>"
"<para><message>%1</message></para>",
QString::fromLocal8Bit(err.asString())),
i18nc("@title:window", "Export Failed"));
}
void ExportGroupsCommand::Private::finishedIfLastJob()
{
if (exportJobs.size() == 0) {
finished();
}
}
void ExportGroupsCommand::Private::cancelJobs()
{
std::for_each(std::cbegin(exportJobs), std::cend(exportJobs),
[](const auto &job) {
if (job) {
job->slotCancel();
}
});
exportJobs.clear();
}
ExportGroupsCommand::ExportGroupsCommand(const std::vector<KeyGroup> &groups)
: Command{new Private{this}}
{
d->groups = groups;
}
ExportGroupsCommand::~ExportGroupsCommand() = default;
void ExportGroupsCommand::doStart()
{
d->start();
}
void ExportGroupsCommand::doCancel()
{
d->cancelJobs();
}
#undef d
#undef q
#include "moc_exportgroupscommand.cpp"
/* -*- mode: c++; c-basic-offset:4 -*-
exportgroupscommand.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2021 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 Kleo
{
class KeyGroup;
class ExportGroupsCommand : public Command
{
Q_OBJECT
public:
explicit ExportGroupsCommand(const std::vector<KeyGroup> &groups);
~ExportGroupsCommand() override;
private:
void doStart() override;
void doCancel() override;
private:
class Private;
inline Private *d_func();
inline const Private *d_func() const;
};
}
......@@ -141,7 +141,7 @@ void ImportCertificateFromFileCommand::doStart()
static QStringList get_file_name(QWidget *parent)
{
const QString certificateFilter = i18n("Certificates") + QLatin1String(" (*.asc *.cer *.cert *.crt *.der *.pem *.gpg *.p7c *.p12 *.pfx *.pgp)");
const QString certificateFilter = i18n("Certificates") + QLatin1String(" (*.asc *.cer *.cert *.crt *.der *.pem *.gpg *.p7c *.p12 *.pfx *.pgp *.kgrp)");
const QString anyFilesFilter = i18n("Any files") + QLatin1String(" (*)");
QString previousDir;
if (const KSharedConfig::Ptr config = KSharedConfig::openConfig()) {
......
......@@ -10,6 +10,7 @@
#include "groupsconfigwidget.h"
#include "commands/exportgroupscommand.h"
#include "dialogs/editgroupdialog.h"
#include <Libkleo/KeyCache>
......@@ -68,6 +69,7 @@ class GroupsConfigWidget::Private
QPushButton *newButton = nullptr;
QPushButton *editButton = nullptr;
QPushButton *deleteButton = nullptr;
QPushButton *exportButton = nullptr;
} ui;
AbstractKeyListModel *groupsModel = nullptr;
ProxyModel *groupsFilterModel = nullptr;
......@@ -115,6 +117,10 @@ public:
ui.deleteButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.deleteButton);
ui.exportButton = new QPushButton{i18nc("@action::button", "Export")};
ui.exportButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.exportButton);
groupsButtonLayout->addStretch(1);
groupsLayout->addLayout(groupsButtonLayout, 1, 1);
......@@ -129,6 +135,7 @@ public:
connect(ui.newButton, &QPushButton::clicked, q, [this] () { addGroup(); });
connect(ui.editButton, &QPushButton::clicked, q, [this] () { editGroup(); });
connect(ui.deleteButton, &QPushButton::clicked, q, [this] () { deleteGroup(); });
connect(ui.exportButton, &QPushButton::clicked, q, [this] () { exportGroup(); });
}
~Private()
......@@ -153,6 +160,7 @@ private:
const bool selectedGroupIsEditable = !selectedGroup.isNull() && !selectedGroup.isImmutable();
ui.editButton->setEnabled(selectedGroupIsEditable);
ui.deleteButton->setEnabled(selectedGroupIsEditable);
ui.exportButton->setEnabled(!selectedGroup.isNull());
}
KeyGroup showEditGroupDialog(KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget)
......@@ -249,6 +257,24 @@ private:
Q_EMIT q->changed();
}
void exportGroup()
{
const QModelIndex groupIndex = selectedIndex();
if (!groupIndex.isValid()) {
qCDebug(KLEOPATRA_LOG) << "selection is empty";
return;
}
const KeyGroup group = getGroup(groupIndex);
if (group.isNull()) {
qCDebug(KLEOPATRA_LOG) << "selected group is null";
return;
}
// execute export group command
auto cmd = new ExportGroupsCommand({group});
cmd->start();
}
};
GroupsConfigWidget::GroupsConfigWidget(QWidget *parent)
......
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