diff --git a/src/commands/certifycertificatecommand.cpp b/src/commands/certifycertificatecommand.cpp
index 152a02899..5c024a9dd 100644
--- a/src/commands/certifycertificatecommand.cpp
+++ b/src/commands/certifycertificatecommand.cpp
@@ -1,338 +1,338 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/certifycertificatecommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
SPDX-FileCopyrightText: 2019 g10code GmbH
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "certifycertificatecommand.h"
#include "newopenpgpcertificatecommand.h"
#include "command_p.h"
#include "dialogs/certifycertificatedialog.h"
#include "exportopenpgpcertstoservercommand.h"
#include "utils/tags.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "kleopatra_debug.h"
#include
using namespace Kleo;
using namespace Kleo::Commands;
using namespace GpgME;
using namespace QGpgME;
class CertifyCertificateCommand::Private : public Command::Private
{
friend class ::Kleo::Commands::CertifyCertificateCommand;
CertifyCertificateCommand *q_func() const
{
return static_cast(q);
}
public:
explicit Private(CertifyCertificateCommand *qq, KeyListController *c);
~Private() override;
void init();
private:
void slotDialogRejected();
void slotResult(const Error &err);
void slotCertificationPrepared();
private:
void ensureDialogCreated();
void createJob();
private:
GpgME::Key target;
std::vector uids;
QPointer dialog;
QPointer job;
};
CertifyCertificateCommand::Private *CertifyCertificateCommand::d_func()
{
return static_cast(d.get());
}
const CertifyCertificateCommand::Private *CertifyCertificateCommand::d_func() const
{
return static_cast(d.get());
}
#define d d_func()
#define q q_func()
CertifyCertificateCommand::Private::Private(CertifyCertificateCommand *qq, KeyListController *c)
: Command::Private(qq, c)
, uids()
, dialog()
, job()
{
}
CertifyCertificateCommand::Private::~Private()
{
qCDebug(KLEOPATRA_LOG);
if (dialog) {
delete dialog;
dialog = nullptr;
}
}
CertifyCertificateCommand::CertifyCertificateCommand(KeyListController *c)
: Command(new Private(this, c))
{
d->init();
}
CertifyCertificateCommand::CertifyCertificateCommand(QAbstractItemView *v, KeyListController *c)
: Command(v, new Private(this, c))
{
d->init();
}
CertifyCertificateCommand::CertifyCertificateCommand(const GpgME::Key &key)
: Command(key, new Private(this, nullptr))
{
d->init();
}
CertifyCertificateCommand::CertifyCertificateCommand(const GpgME::UserID &uid)
: Command(uid.parent(), new Private(this, nullptr))
{
std::vector(1, uid).swap(d->uids);
d->init();
}
CertifyCertificateCommand::CertifyCertificateCommand(const std::vector &uids)
: Command(uids.empty() ? Key() : uids.front().parent(), new Private(this, nullptr))
{
d->uids = uids;
d->init();
}
void CertifyCertificateCommand::Private::init()
{
}
CertifyCertificateCommand::~CertifyCertificateCommand()
{
qCDebug(KLEOPATRA_LOG);
}
void CertifyCertificateCommand::doStart()
{
const std::vector keys = d->keys();
if (keys.size() != 1 || keys.front().protocol() != GpgME::OpenPGP) {
d->finished();
return;
}
// hold on to the key to certify to avoid invalidation during refreshes of the key cache
d->target = keys.front();
if (d->target.isExpired() || d->target.isRevoked()) {
const auto title = d->target.isRevoked() ? i18nc("@title:window", "Key is Revoked") : i18nc("@title:window", "Key is Expired");
const auto message = d->target.isRevoked() //
? i18nc("@info", "This key has been revoked. You cannot certify it.")
: i18nc("@info", "This key has expired. You cannot certify it.");
d->information(message, title);
d->finished();
return;
}
auto findAnyGoodKey = []() {
const std::vector secKeys = KeyCache::instance()->secretKeys();
return std::any_of(secKeys.cbegin(), secKeys.cend(), [](const Key &secKey) {
return Kleo::keyHasCertify(secKey) && secKey.protocol() == OpenPGP && !secKey.isRevoked() && !secKey.isExpired() && !secKey.isInvalid();
});
};
if (!findAnyGoodKey()) {
auto sel =
KMessageBox::questionTwoActions(d->parentWidgetOrView(),
xi18nc("@info", "To certify other certificates, you first need to create an OpenPGP certificate for yourself.")
+ QStringLiteral("
") + i18n("Do you wish to create one now?"),
i18nc("@title:window", "Certification Not Possible"),
KGuiItem(i18n("Create")),
KStandardGuiItem::cancel());
if (sel == KMessageBox::ButtonCode::PrimaryAction) {
QEventLoop loop;
auto cmd = new NewOpenPGPCertificateCommand;
cmd->setParentWidget(d->parentWidgetOrView());
connect(cmd, &Command::finished, &loop, &QEventLoop::quit);
QMetaObject::invokeMethod(cmd, &NewOpenPGPCertificateCommand::start, Qt::QueuedConnection);
loop.exec();
} else {
Q_EMIT(canceled());
d->finished();
return;
}
// Check again for secret keys
if (!findAnyGoodKey()) {
qCDebug(KLEOPATRA_LOG) << "Sec Keys still empty after keygen.";
Q_EMIT(canceled());
d->finished();
return;
}
}
const char *primary = keys.front().primaryFingerprint();
const bool anyMismatch = std::any_of(d->uids.cbegin(), d->uids.cend(), [primary](const UserID &uid) {
return qstricmp(uid.parent().primaryFingerprint(), primary) != 0;
});
if (anyMismatch) {
qCWarning(KLEOPATRA_LOG) << "User ID <-> Key mismatch!";
d->finished();
return;
}
d->ensureDialogCreated();
Q_ASSERT(d->dialog);
d->dialog->setCertificateToCertify(d->target, d->uids);
d->dialog->show();
}
void CertifyCertificateCommand::Private::slotDialogRejected()
{
Q_EMIT q->canceled();
finished();
}
void CertifyCertificateCommand::Private::slotResult(const Error &err)
{
if (err.isCanceled()) {
// do nothing
} else if (err) {
error(i18n("An error occurred while trying to certify
"
"%1:
\t%2
",
Formatting::formatForComboBox(target),
Formatting::errorAsString(err)),
i18n("Certification Error"));
} else if (dialog && dialog->exportableCertificationSelected() && dialog->sendToServer()) {
auto const cmd = new ExportOpenPGPCertsToServerCommand(target);
cmd->start();
} else {
information(i18n("Certification successful."), i18n("Certification Succeeded"));
}
if (!dialog->tags().isEmpty()) {
Tags::enableTags();
}
finished();
}
void CertifyCertificateCommand::Private::slotCertificationPrepared()
{
Q_ASSERT(dialog);
const auto selectedUserIds = dialog->selectedUserIDs();
std::vector userIdIndexes;
userIdIndexes.reserve(selectedUserIds.size());
for (unsigned int i = 0, numUserIds = target.numUserIDs(); i < numUserIds; ++i) {
const auto userId = target.userID(i);
- const bool userIdIsSelected = Kleo::any_of(selectedUserIds, [userId](const auto &uid) {
+ const bool userIdIsSelected = std::ranges::any_of(selectedUserIds, [userId](const auto &uid) {
return Kleo::userIDsAreEqual(userId, uid);
});
if (userIdIsSelected) {
userIdIndexes.push_back(i);
}
}
createJob();
Q_ASSERT(job);
job->setExportable(dialog->exportableCertificationSelected());
job->setUserIDsToSign(userIdIndexes);
job->setSigningKey(dialog->selectedSecretKey());
if (!dialog->tags().isEmpty()) {
// do not set an empty remark to avoid an empty signature notation (GnuPG bug T5142)
job->setRemark(dialog->tags());
}
job->setDupeOk(true);
if (dialog->trustSignatureSelected() && !dialog->trustSignatureDomain().isEmpty()) {
// always create level 1 trust signatures with complete trust
job->setTrustSignature(TrustSignatureTrust::Complete, 1, dialog->trustSignatureDomain());
}
if (!dialog->expirationDate().isNull()) {
job->setExpirationDate(dialog->expirationDate());
}
if (const Error err = job->start(target)) {
slotResult(err);
}
}
void CertifyCertificateCommand::doCancel()
{
qCDebug(KLEOPATRA_LOG);
if (d->job) {
d->job->slotCancel();
}
}
void CertifyCertificateCommand::Private::ensureDialogCreated()
{
if (dialog) {
return;
}
dialog = new CertifyCertificateDialog;
applyWindowID(dialog);
connect(dialog, &QDialog::rejected, q, [this]() {
slotDialogRejected();
});
connect(dialog, &QDialog::accepted, q, [this]() {
slotCertificationPrepared();
});
}
void CertifyCertificateCommand::Private::createJob()
{
Q_ASSERT(!job);
Q_ASSERT(target.protocol() == OpenPGP);
const auto backend = QGpgME::openpgp();
if (!backend) {
return;
}
SignKeyJob *const j = backend->signKeyJob();
if (!j) {
return;
}
connect(j, &QGpgME::Job::jobProgress, q, &Command::progress);
connect(j, &SignKeyJob::result, q, [this](const GpgME::Error &result) {
slotResult(result);
});
job = j;
}
#undef d
#undef q
#include "moc_certifycertificatecommand.cpp"
diff --git a/src/commands/changepassphrasecommand.h b/src/commands/changepassphrasecommand.h
index 4e7d52bcb..ee331ea92 100644
--- a/src/commands/changepassphrasecommand.h
+++ b/src/commands/changepassphrasecommand.h
@@ -1,44 +1,44 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/changepassphrasecommand.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include
namespace Kleo
{
namespace Commands
{
class ChangePassphraseCommand : public Command
{
Q_OBJECT
public:
explicit ChangePassphraseCommand(QAbstractItemView *view, KeyListController *parent);
explicit ChangePassphraseCommand(KeyListController *parent);
explicit ChangePassphraseCommand(const GpgME::Key &key);
~ChangePassphraseCommand() override;
/* reimp */ static Restrictions restrictions()
{
- return OnlyOneKey | NeedSecretKeyData;
+ return OnlyOneKey | NeedSecretSubkeyData;
}
private:
void doStart() override;
void doCancel() override;
private:
class Private;
inline Private *d_func();
inline const Private *d_func() const;
};
}
}
diff --git a/src/commands/command.h b/src/commands/command.h
index b3da77b66..40a0b036f 100644
--- a/src/commands/command.h
+++ b/src/commands/command.h
@@ -1,139 +1,140 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/command.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include
#include // for WId
#include
#include // for ExecutionContext
#include
class QModelIndex;
template
class QList;
class QAbstractItemView;
namespace GpgME
{
class Key;
}
namespace Kleo
{
class KeyListController;
class AbstractKeyListSortFilterProxyModel;
class Command : public QObject, public ExecutionContext
{
Q_OBJECT
public:
explicit Command(KeyListController *parent);
explicit Command(QAbstractItemView *view, KeyListController *parent);
explicit Command(const GpgME::Key &key);
explicit Command(const std::vector &keys);
~Command() override;
enum Restriction {
// clang-format off
NoRestriction = 0x0000,
NeedSelection = 0x0001,
OnlyOneKey = 0x0002,
NeedSecretKey = 0x0004, //< command performs secret key operations
- NeedSecretKeyData = 0x0008, //< command needs access to the secret key data
- MustBeOpenPGP = 0x0010,
- MustBeCMS = 0x0020,
+ NeedSecretPrimaryKeyData = 0x0008, //< command needs access to the secret key data of the primary key
+ NeedSecretSubkeyData = 0x0010, //< command needs access to the secret key data of one or more subkeys
// esoteric:
MayOnlyBeSecretKeyIfOwnerTrustIsNotYetUltimate = 0x0040, // for set-owner-trust
AnyCardHasNullPin = 0x0080,
MustBeRoot = 0x0200,
MustBeTrustedRoot = 0x0400 | MustBeRoot,
MustBeUntrustedRoot = 0x0800 | MustBeRoot,
MustBeValid = 0x1000, //< key is neither revoked nor expired nor otherwise "bad"
+ MustBeOpenPGP = 0x2000,
+ MustBeCMS = 0x4000,
_AllRestrictions_Helper,
AllRestrictions = 2 * (_AllRestrictions_Helper - 1) - 1,
// clang-format on
};
Q_DECLARE_FLAGS(Restrictions, Restriction)
static Restrictions restrictions()
{
return NoRestriction;
}
/** Classify the files and return the most appropriate commands.
*
* @param files: A list of files.
*
* @returns null QString on success. Error message otherwise.
*/
static QList commandsForFiles(const QStringList &files, KeyListController *controller);
/** Get a command for a query.
*
* @param query: A keyid / fingerprint or any string to use in the search.
*/
static Command *commandForQuery(const QString &query);
void setParentWidget(QWidget *widget);
void setParentWId(WId wid);
void setView(QAbstractItemView *view);
void setKey(const GpgME::Key &key);
void setKeys(const std::vector &keys);
void setAutoDelete(bool on);
bool autoDelete() const;
void setWarnWhenRunningAtShutdown(bool warn);
bool warnWhenRunningAtShutdown() const;
public Q_SLOTS:
void start();
void cancel();
Q_SIGNALS:
void info(const QString &message, int timeout = 0);
void progress(int current, int total);
void finished();
void canceled();
private:
virtual void doStart() = 0;
virtual void doCancel() = 0;
private:
void applyWindowID(QWidget *wid) const override;
protected:
void addTemporaryView(const QString &title, AbstractKeyListSortFilterProxyModel *proxy = nullptr, const QString &tabToolTip = QString());
protected:
class Private;
kdtools::pimpl_ptr d;
protected:
explicit Command(Private *pp);
explicit Command(QAbstractItemView *view, Private *pp);
explicit Command(const std::vector &keys, Private *pp);
explicit Command(const GpgME::Key &key, Private *pp);
};
}
Q_DECLARE_OPERATORS_FOR_FLAGS(Kleo::Command::Restrictions)
diff --git a/src/commands/exportcertificatecommand.cpp b/src/commands/exportcertificatecommand.cpp
index 3551af6d0..77cd65f60 100644
--- a/src/commands/exportcertificatecommand.cpp
+++ b/src/commands/exportcertificatecommand.cpp
@@ -1,411 +1,411 @@
/* -*- mode: c++; c-basic-offset:4 -*-
exportcertificatecommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
SPDX-FileCopyrightText: 2021 g10 Code GmbH
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "exportcertificatecommand.h"
#include "fileoperationspreferences.h"
#include "command_p.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace Kleo;
using namespace GpgME;
using namespace QGpgME;
class ExportCertificateCommand::Private : public Command::Private
{
friend class ::ExportCertificateCommand;
ExportCertificateCommand *q_func() const
{
return static_cast(q);
}
public:
explicit Private(ExportCertificateCommand *qq, KeyListController *c);
~Private() override;
void startExportJob(GpgME::Protocol protocol, const std::vector &keys);
void cancelJobs();
void exportResult(const GpgME::Error &, const QByteArray &);
void showError(const GpgME::Error &error);
bool confirmExport(const std::vector &pgpKeys);
bool requestFileNames(GpgME::Protocol prot);
void finishedIfLastJob();
private:
QMap fileNames;
uint jobsPending = 0;
QMap outFileForSender;
QPointer cmsJob;
QPointer pgpJob;
};
ExportCertificateCommand::Private *ExportCertificateCommand::d_func()
{
return static_cast(d.get());
}
const ExportCertificateCommand::Private *ExportCertificateCommand::d_func() const
{
return static_cast(d.get());
}
#define d d_func()
#define q q_func()
ExportCertificateCommand::Private::Private(ExportCertificateCommand *qq, KeyListController *c)
: Command::Private(qq, c)
{
}
ExportCertificateCommand::Private::~Private()
{
}
ExportCertificateCommand::ExportCertificateCommand(KeyListController *p)
: Command(new Private(this, p))
{
}
ExportCertificateCommand::ExportCertificateCommand(QAbstractItemView *v, KeyListController *p)
: Command(v, new Private(this, p))
{
}
ExportCertificateCommand::ExportCertificateCommand(const Key &key)
: Command(key, new Private(this, nullptr))
{
}
ExportCertificateCommand::~ExportCertificateCommand()
{
}
void ExportCertificateCommand::setOpenPGPFileName(const QString &fileName)
{
if (!d->jobsPending) {
d->fileNames[OpenPGP] = fileName;
}
}
QString ExportCertificateCommand::openPGPFileName() const
{
return d->fileNames[OpenPGP];
}
void ExportCertificateCommand::setX509FileName(const QString &fileName)
{
if (!d->jobsPending) {
d->fileNames[CMS] = fileName;
}
}
QString ExportCertificateCommand::x509FileName() const
{
return d->fileNames[CMS];
}
void ExportCertificateCommand::doStart()
{
if (d->keys().empty()) {
d->finished();
return;
}
const auto keys = Kleo::partitionKeysByProtocol(d->keys());
if (!keys.openpgp.empty() && !d->confirmExport(keys.openpgp)) {
d->canceled();
return;
}
const bool haveBoth = !keys.cms.empty() && !keys.openpgp.empty();
const GpgME::Protocol prot = haveBoth ? UnknownProtocol : (!keys.cms.empty() ? CMS : OpenPGP);
if (!d->requestFileNames(prot)) {
d->canceled();
return;
}
if (!keys.openpgp.empty()) {
d->startExportJob(GpgME::OpenPGP, keys.openpgp);
}
if (!keys.cms.empty()) {
d->startExportJob(GpgME::CMS, keys.cms);
}
}
bool ExportCertificateCommand::Private::confirmExport(const std::vector &pgpKeys)
{
auto notCertifiedKeys = std::accumulate(pgpKeys.cbegin(), pgpKeys.cend(), QStringList{}, [](auto keyNames, const auto &key) {
- const bool allValidUserIDsAreCertifiedByUser = Kleo::all_of(key.userIDs(), [](const UserID &userId) {
+ const bool allValidUserIDsAreCertifiedByUser = std::ranges::all_of(key.userIDs(), [](const UserID &userId) {
return userId.isBad() || Kleo::userIDIsCertifiedByUser(userId);
});
if (!allValidUserIDsAreCertifiedByUser) {
keyNames.push_back(Formatting::formatForComboBox(key));
}
return keyNames;
});
if (!notCertifiedKeys.empty()) {
if (pgpKeys.size() == 1) {
const auto answer = KMessageBox::warningContinueCancel( //
parentWidgetOrView(),
xi18nc("@info",
"You haven't certified all valid user IDs of this certificate "
"with an exportable certification. People relying on your certifications "
"may not be able to verify the certificate."
"Do you want to continue the export?"),
i18nc("@title:window", "Confirm Certificate Export"),
KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", 1)},
KStandardGuiItem::cancel(),
QStringLiteral("confirm-export-of-uncertified-keys"));
return answer == KMessageBox::Continue;
} else {
std::sort(notCertifiedKeys.begin(), notCertifiedKeys.end());
const auto answer = KMessageBox::warningContinueCancelList( //
parentWidgetOrView(),
xi18nc("@info",
"You haven't certified all valid user IDs of the certificates listed below "
"with exportable certifications. People relying on your certifications "
"may not be able to verify the certificates."
"Do you want to continue the export?"),
notCertifiedKeys,
i18nc("@title:window", "Confirm Certificate Export"),
KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", pgpKeys.size())},
KStandardGuiItem::cancel(),
QStringLiteral("confirm-export-of-uncertified-keys"));
return answer == KMessageBox::Continue;
}
}
return true;
}
bool ExportCertificateCommand::Private::requestFileNames(GpgME::Protocol protocol)
{
if (protocol == UnknownProtocol) {
if (!fileNames[GpgME::OpenPGP].isEmpty() && !fileNames[GpgME::CMS].isEmpty()) {
return true;
}
/* Unknown protocol ask for first PGP Export file name */
if (fileNames[GpgME::OpenPGP].isEmpty() && !requestFileNames(GpgME::OpenPGP)) {
return false;
}
/* And then for CMS */
return requestFileNames(GpgME::CMS);
}
if (!fileNames[protocol].isEmpty()) {
return true;
}
const auto lastDir = ApplicationState::lastUsedExportDirectory();
QString proposedFileName = lastDir + QLatin1Char('/');
if (keys().size() == 1) {
const bool usePGPFileExt = FileOperationsPreferences().usePGPFileExt();
const auto key = keys().front();
auto name = Formatting::prettyName(key);
if (name.isEmpty()) {
name = Formatting::prettyEMail(key);
}
const auto asciiArmoredCertificateClass = (protocol == OpenPGP ? Class::OpenPGP : Class::CMS) | Class::Ascii | Class::Certificate;
/* Not translated so it's better to use in tutorials etc. */
proposedFileName += QStringLiteral("%1_%2_public.%3")
.arg(name)
.arg(Formatting::prettyKeyID(key.shortKeyID()))
.arg(outputFileExtension(asciiArmoredCertificateClass, usePGPFileExt));
}
if (protocol == GpgME::CMS) {
if (!fileNames[GpgME::OpenPGP].isEmpty()) {
/* If the user has already selected a PGP file name then use that as basis
* for a proposal for the S/MIME file. */
proposedFileName = fileNames[GpgME::OpenPGP];
const int idx = proposedFileName.size() - 4;
if (proposedFileName.endsWith(QLatin1StringView(".asc"))) {
proposedFileName.replace(idx, 4, QLatin1StringView(".pem"));
}
if (proposedFileName.endsWith(QLatin1StringView(".gpg")) || proposedFileName.endsWith(QLatin1String(".pgp"))) {
proposedFileName.replace(idx, 4, QLatin1StringView(".der"));
}
}
}
if (proposedFileName.isEmpty()) {
proposedFileName = lastDir;
proposedFileName += i18nc("A generic filename for exported certificates", "certificates");
proposedFileName += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem");
}
auto fname = FileDialog::getSaveFileNameEx(parentWidgetOrView(),
i18nc("1 is protocol", "Export %1 Certificates", Formatting::displayName(protocol)),
QStringLiteral("imp"),
proposedFileName,
protocol == GpgME::OpenPGP ? i18n("OpenPGP Certificates") + QLatin1StringView(" (*.asc *.gpg *.pgp)")
: i18n("S/MIME Certificates") + QLatin1StringView(" (*.pem *.der)"));
if (!fname.isEmpty() && protocol == GpgME::CMS && fileNames[GpgME::OpenPGP] == fname) {
KMessageBox::error(parentWidgetOrView(),
i18n("You have to select different filenames for different protocols."),
i18nc("@title:window", "Export Error"));
return false;
}
const QFileInfo fi(fname);
if (fi.suffix().isEmpty()) {
fname += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem");
}
fileNames[protocol] = fname;
ApplicationState::setLastUsedExportDirectory(fi.absolutePath());
return !fname.isEmpty();
}
void ExportCertificateCommand::Private::startExportJob(GpgME::Protocol protocol, const std::vector &keys)
{
Q_ASSERT(protocol != GpgME::UnknownProtocol);
const QGpgME::Protocol *const backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
Q_ASSERT(backend);
const QString fileName = fileNames[protocol];
const bool binary = protocol == GpgME::OpenPGP
? fileName.endsWith(QLatin1StringView(".gpg"), Qt::CaseInsensitive) || fileName.endsWith(QLatin1String(".pgp"), Qt::CaseInsensitive)
: fileName.endsWith(QLatin1StringView(".der"), Qt::CaseInsensitive);
std::unique_ptr job(backend->publicKeyExportJob(!binary));
Q_ASSERT(job.get());
connect(job.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &result, const QByteArray &keyData) {
exportResult(result, keyData);
});
connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress);
QStringList fingerprints;
fingerprints.reserve(keys.size());
for (const Key &i : keys) {
fingerprints << QLatin1StringView(i.primaryFingerprint());
}
const GpgME::Error err = job->start(fingerprints);
if (err) {
showError(err);
finished();
return;
}
Q_EMIT q->info(i18n("Exporting certificates..."));
++jobsPending;
const QPointer exportJob(job.release());
outFileForSender[exportJob.data()] = fileName;
(protocol == CMS ? cmsJob : pgpJob) = exportJob;
}
void ExportCertificateCommand::Private::showError(const GpgME::Error &err)
{
Q_ASSERT(err);
const QString msg = i18n(
"An error occurred while trying to export "
"the certificate:
"
"%1
",
Formatting::errorAsString(err));
error(msg, i18n("Certificate Export Failed"));
}
void ExportCertificateCommand::doCancel()
{
d->cancelJobs();
}
void ExportCertificateCommand::Private::finishedIfLastJob()
{
if (jobsPending <= 0) {
finished();
}
}
static bool write_complete(QIODevice &iod, const QByteArray &data)
{
qint64 total = 0;
qint64 toWrite = data.size();
while (total < toWrite) {
const qint64 written = iod.write(data.data() + total, toWrite);
if (written < 0) {
return false;
}
total += written;
toWrite -= written;
}
return true;
}
void ExportCertificateCommand::Private::exportResult(const GpgME::Error &err, const QByteArray &data)
{
Q_ASSERT(jobsPending > 0);
--jobsPending;
Q_ASSERT(outFileForSender.contains(q->sender()));
const QString outFile = outFileForSender[q->sender()];
if (err) {
showError(err);
finishedIfLastJob();
return;
}
QSaveFile savefile(outFile);
// TODO: use KIO
const QString writeErrorMsg = i18n("Could not write to file %1.", outFile);
const QString errorCaption = i18n("Certificate Export Failed");
if (!savefile.open(QIODevice::WriteOnly)) {
error(writeErrorMsg, errorCaption);
finishedIfLastJob();
return;
}
if (!write_complete(savefile, data) || !savefile.commit()) {
error(writeErrorMsg, errorCaption);
}
finishedIfLastJob();
}
void ExportCertificateCommand::Private::cancelJobs()
{
if (cmsJob) {
cmsJob->slotCancel();
}
if (pgpJob) {
pgpJob->slotCancel();
}
}
#undef d
#undef q
#include "moc_exportcertificatecommand.cpp"
diff --git a/src/commands/exportgroupscommand.cpp b/src/commands/exportgroupscommand.cpp
index 295068787..c7664a257 100644
--- a/src/commands/exportgroupscommand.cpp
+++ b/src/commands/exportgroupscommand.cpp
@@ -1,335 +1,335 @@
/* -*- 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
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "command_p.h"
#include "exportgroupscommand.h"
#include "utils/filedialog.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace Kleo;
using namespace GpgME;
using namespace QGpgME;
namespace
{
static const QString certificateGroupFileExtension{QLatin1StringView{".kgrp"}};
QString proposeFilename(const std::vector &groups)
{
QString filename;
filename = ApplicationState::lastUsedExportDirectory() + 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 &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;
}
ApplicationState::setLastUsedExportDirectory(filename);
}
return filename;
}
}
class ExportGroupsCommand::Private : public Command::Private
{
friend class ::ExportGroupsCommand;
ExportGroupsCommand *q_func() const
{
return static_cast(q);
}
public:
explicit Private(ExportGroupsCommand *qq);
~Private() override;
void start();
bool confirmExport();
bool exportGroups();
bool startExportJob(GpgME::Protocol protocol, const std::vector &keys);
void onExportJobResult(const QGpgME::Job *job, const GpgME::Error &err, const QByteArray &keyData);
void cancelJobs();
void showError(const GpgME::Error &err);
void finishedIfLastJob(const QGpgME::Job *job);
private:
std::vector groups;
QString filename;
std::vector> exportJobs;
};
ExportGroupsCommand::Private *ExportGroupsCommand::d_func()
{
return static_cast(d.get());
}
const ExportGroupsCommand::Private *ExportGroupsCommand::d_func() const
{
return static_cast(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;
}
if (!confirmExport()) {
canceled();
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;
});
const auto keys = Kleo::partitionKeysByProtocol(groupKeys);
// remove/overwrite existing file
if (QFile::exists(filename) && !QFile::remove(filename)) {
error(xi18n("Cannot overwrite existing %1.", filename), i18nc("@title:window", "Export Failed"));
finished();
return;
}
if (!exportGroups()) {
finished();
return;
}
if (!keys.openpgp.empty()) {
if (!startExportJob(GpgME::OpenPGP, keys.openpgp)) {
finished();
return;
}
}
if (!keys.cms.empty()) {
if (!startExportJob(GpgME::CMS, keys.cms)) {
finishedIfLastJob(nullptr);
}
}
}
bool ExportGroupsCommand::Private::confirmExport()
{
auto notFullyCertifiedGroups = std::accumulate(groups.cbegin(), groups.cend(), QStringList{}, [](auto groupNames, const auto &group) {
- const bool allOpenPGPKeysAreCertifiedByUser = Kleo::all_of(group.keys(), [](const Key &key) {
+ const bool allOpenPGPKeysAreCertifiedByUser = std::ranges::all_of(group.keys(), [](const Key &key) {
// we only check the primary user ID of OpenPGP keys because currently group certification only certifies the primary user ID
return key.protocol() != GpgME::OpenPGP || Kleo::userIDIsCertifiedByUser(key.userID(0));
});
if (!allOpenPGPKeysAreCertifiedByUser) {
groupNames.push_back(group.name());
}
return groupNames;
});
if (!notFullyCertifiedGroups.empty()) {
if (groups.size() == 1) {
const auto answer = KMessageBox::warningContinueCancel( //
parentWidgetOrView(),
xi18nc("@info",
"You haven't certified all OpenPGP certificates in this group "
"with an exportable certification. People relying on your certifications "
"may not be able to verify the certificates."
"Do you want to continue the export?"),
i18nc("@title:window", "Confirm Group Export"),
KGuiItem{i18nc("@action:button", "Export Group")},
KStandardGuiItem::cancel(),
QStringLiteral("confirm-export-of-uncertified-groups"));
return answer == KMessageBox::Continue;
} else {
std::sort(notFullyCertifiedGroups.begin(), notFullyCertifiedGroups.end());
const auto answer = KMessageBox::warningContinueCancelList( //
parentWidgetOrView(),
xi18nc("@info",
"You haven't certified all OpenPGP certificates in the groups listed below "
"with exportable certifications. People relying on your certifications "
"may not be able to verify the certificates."
"Do you want to continue the export?"),
notFullyCertifiedGroups,
i18nc("@title:window", "Confirm Group Export"),
KGuiItem{i18nc("@action:button", "Export Groups")},
KStandardGuiItem::cancel(),
QStringLiteral("confirm-export-of-uncertified-groups"));
return answer == KMessageBox::Continue;
}
}
return true;
}
bool ExportGroupsCommand::Private::exportGroups()
{
const auto result = writeKeyGroups(filename, groups);
if (result != WriteKeyGroups::Success) {
error(xi18n("Writing groups to file %1 failed.", filename), i18nc("@title:window", "Export Failed"));
}
return result == WriteKeyGroups::Success;
}
bool ExportGroupsCommand::Private::startExportJob(GpgME::Protocol protocol, const std::vector &keys)
{
const QGpgME::Protocol *const backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
Q_ASSERT(backend);
std::unique_ptr 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, &QGpgME::Job::jobProgress, 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));
if (err) {
showError(err);
finishedIfLastJob(job);
return;
}
QFile f{filename};
if (!f.open(QIODevice::WriteOnly | QIODevice::Append)) {
error(xi18n("Cannot open file %1 for writing.", filename), i18nc("@title:window", "Export Failed"));
finishedIfLastJob(job);
return;
}
const auto bytesWritten = f.write(keyData);
if (bytesWritten != keyData.size()) {
error(xi18n("Writing certificates to file %1 failed.", filename), i18nc("@title:window", "Export Failed"));
}
finishedIfLastJob(job);
}
void ExportGroupsCommand::Private::showError(const GpgME::Error &err)
{
error(xi18n("An error occurred during the export:"
"%1",
Formatting::errorAsString(err)),
i18nc("@title:window", "Export Failed"));
}
void ExportGroupsCommand::Private::finishedIfLastJob(const QGpgME::Job *job)
{
if (job) {
exportJobs.erase(std::remove(exportJobs.begin(), exportJobs.end(), job), exportJobs.end());
}
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 &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"
diff --git a/src/commands/exportopenpgpcertstoservercommand.cpp b/src/commands/exportopenpgpcertstoservercommand.cpp
index fadc02544..c37808ce2 100644
--- a/src/commands/exportopenpgpcertstoservercommand.cpp
+++ b/src/commands/exportopenpgpcertstoservercommand.cpp
@@ -1,179 +1,179 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/exportopenpgpcertstoservercommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "exportopenpgpcertstoservercommand.h"
#include "command_p.h"
#include
#include
#include
#include
#include
#include
#include
using namespace Kleo;
using namespace Kleo::Commands;
using namespace GpgME;
ExportOpenPGPCertsToServerCommand::ExportOpenPGPCertsToServerCommand(KeyListController *c)
: GnuPGProcessCommand(c)
{
}
ExportOpenPGPCertsToServerCommand::ExportOpenPGPCertsToServerCommand(QAbstractItemView *v, KeyListController *c)
: GnuPGProcessCommand(v, c)
{
}
ExportOpenPGPCertsToServerCommand::ExportOpenPGPCertsToServerCommand(const Key &key)
: GnuPGProcessCommand(key)
{
}
ExportOpenPGPCertsToServerCommand::ExportOpenPGPCertsToServerCommand(const std::vector &keys)
: GnuPGProcessCommand(keys)
{
}
ExportOpenPGPCertsToServerCommand::~ExportOpenPGPCertsToServerCommand() = default;
static bool confirmExport(const std::vector &pgpKeys, QWidget *parentWidget)
{
auto notCertifiedKeys = std::accumulate(pgpKeys.cbegin(), pgpKeys.cend(), QStringList{}, [](auto keyNames, const auto &key) {
- const bool allValidUserIDsAreCertifiedByUser = Kleo::all_of(key.userIDs(), [](const UserID &userId) {
+ const bool allValidUserIDsAreCertifiedByUser = std::ranges::all_of(key.userIDs(), [](const UserID &userId) {
return userId.isBad() || Kleo::userIDIsCertifiedByUser(userId);
});
if (!allValidUserIDsAreCertifiedByUser) {
keyNames.push_back(Formatting::formatForComboBox(key));
}
return keyNames;
});
if (!notCertifiedKeys.empty()) {
if (pgpKeys.size() == 1) {
const auto answer = KMessageBox::warningContinueCancel( //
parentWidget,
xi18nc("@info",
"You haven't certified all valid user IDs of this certificate "
"with an exportable certification. People relying on your certifications "
"may not be able to verify the certificate."
"Do you want to continue the export?"),
i18nc("@title:window", "Confirm Certificate Export"),
KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", 1)},
KStandardGuiItem::cancel(),
QStringLiteral("confirm-upload-of-uncertified-keys"));
return answer == KMessageBox::Continue;
} else {
std::sort(notCertifiedKeys.begin(), notCertifiedKeys.end());
const auto answer = KMessageBox::warningContinueCancelList( //
parentWidget,
xi18nc("@info",
"You haven't certified all valid user IDs of the certificates listed below "
"with exportable certifications. People relying on your certifications "
"may not be able to verify the certificates."
"Do you want to continue the export?"),
notCertifiedKeys,
i18nc("@title:window", "Confirm Certificate Export"),
KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", pgpKeys.size())},
KStandardGuiItem::cancel(),
QStringLiteral("confirm-upload-of-uncertified-keys"));
return answer == KMessageBox::Continue;
}
}
return true;
}
bool ExportOpenPGPCertsToServerCommand::preStartHook(QWidget *parent) const
{
if (!haveKeyserverConfigured()) {
d->error(i18ncp("@info",
"Exporting the certificate to a key server is not possible "
"because the usage of key servers has been disabled explicitly.",
"Exporting the certificates to a key server is not possible "
"because the usage of key servers has been disabled explicitly.",
d->keys().size()));
return false;
}
if (!confirmExport(d->keys(), parent)) {
return false;
}
return KMessageBox::warningContinueCancel(parent,
xi18nc("@info",
"When OpenPGP certificates have been exported to a public directory server, "
"it is nearly impossible to remove them again."
"Before exporting your certificate to a public directory server, make sure that you "
"have created a revocation certificate so you can revoke the certificate if needed later."
"Are you sure you want to continue?"),
i18nc("@title:window", "OpenPGP Certificate Export"),
KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", d->keys().size())},
KStandardGuiItem::cancel(),
QStringLiteral("warn-export-openpgp-nonrevocable"))
== KMessageBox::Continue;
}
QStringList ExportOpenPGPCertsToServerCommand::arguments() const
{
QStringList result;
result << gpgPath();
result << QStringLiteral("--send-keys");
const auto keys = d->keys();
for (const Key &key : keys) {
result << QLatin1StringView(key.primaryFingerprint());
}
return result;
}
QString ExportOpenPGPCertsToServerCommand::errorCaption() const
{
return i18nc("@title:window", "OpenPGP Certificate Export Error");
}
QString ExportOpenPGPCertsToServerCommand::successCaption() const
{
return i18nc("@title:window", "OpenPGP Certificate Export Finished");
}
QString ExportOpenPGPCertsToServerCommand::crashExitMessage(const QStringList &args) const
{
return xi18nc("@info",
"The GPG process that tried to export OpenPGP certificates "
"ended prematurely because of an unexpected error."
"Please check the output of %1 for details.",
args.join(QLatin1Char(' ')));
}
QString ExportOpenPGPCertsToServerCommand::errorExitMessage(const QStringList &args) const
{
// ki18n(" ") as initializer because initializing with empty string leads to
// (I18N_EMPTY_MESSAGE)
const auto errorLines = errorString().split(QLatin1Char{'\n'});
const auto errorText = std::accumulate(errorLines.begin(), errorLines.end(), KLocalizedString{ki18n(" ")}, [](KLocalizedString temp, const auto &line) {
return kxi18nc("@info used for concatenating multiple lines of text with line breaks; most likely this shouldn't be translated", "%1%2")
.subs(temp)
.subs(line);
});
return xi18nc("@info",
"An error occurred while trying to export OpenPGP certificates. "
"The output of %1 was:%2",
args[0],
errorText);
}
QString ExportOpenPGPCertsToServerCommand::successMessage(const QStringList &) const
{
return i18nc("@info", "OpenPGP certificates exported successfully.");
}
#include "moc_exportopenpgpcertstoservercommand.cpp"
diff --git a/src/commands/exportpaperkeycommand.h b/src/commands/exportpaperkeycommand.h
index 1586e6faa..fdf324cdb 100644
--- a/src/commands/exportpaperkeycommand.h
+++ b/src/commands/exportpaperkeycommand.h
@@ -1,46 +1,46 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/exportpaperkeycommand.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik
SPDX-FileContributor: Intevation GmbH
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include
#include
#include
class QWidget;
namespace Kleo
{
namespace Commands
{
class ExportPaperKeyCommand : public Command
{
Q_OBJECT
public:
explicit ExportPaperKeyCommand(QAbstractItemView *view, KeyListController *parent);
static Restrictions restrictions()
{
- return OnlyOneKey | NeedSecretKeyData | MustBeOpenPGP;
+ return OnlyOneKey | NeedSecretPrimaryKeyData | MustBeOpenPGP;
}
private:
class Private;
inline Private *d_func();
inline const Private *d_func() const;
void doStart() override;
void doCancel() override;
};
}
}
diff --git a/src/commands/exportsecretkeycommand.h b/src/commands/exportsecretkeycommand.h
index 451199850..5974ecd79 100644
--- a/src/commands/exportsecretkeycommand.h
+++ b/src/commands/exportsecretkeycommand.h
@@ -1,44 +1,44 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/exportsecretkeycommand.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "command.h"
namespace Kleo
{
namespace Commands
{
class ExportSecretKeyCommand : public Command
{
Q_OBJECT
public:
explicit ExportSecretKeyCommand(QAbstractItemView *view, KeyListController *parent);
explicit ExportSecretKeyCommand(const GpgME::Key &key);
~ExportSecretKeyCommand() override;
/* reimp */ static Restrictions restrictions()
{
- return OnlyOneKey | NeedSecretKeyData;
+ return OnlyOneKey | NeedSecretPrimaryKeyData;
}
private:
void doStart() override;
void doCancel() override;
private:
class Private;
inline Private *d_func();
inline const Private *d_func() const;
};
}
}
diff --git a/src/commands/importcertificatescommand.cpp b/src/commands/importcertificatescommand.cpp
index 44fa51ab9..7788dd204 100644
--- a/src/commands/importcertificatescommand.cpp
+++ b/src/commands/importcertificatescommand.cpp
@@ -1,1104 +1,1104 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/importcertificatescommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2007, 2008 Klarälvdalens Datakonsult AB
SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik
SPDX-FileContributor: Intevation GmbH
SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "importcertificatescommand.h"
#include "importcertificatescommand_p.h"
#include "certifycertificatecommand.h"
#include "kleopatra_debug.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
"};
}
report += QLatin1StringView{""};
return report;
}
// Returns false on error, true if please certify was shown.
bool ImportCertificatesCommand::Private::showPleaseCertify(const GpgME::Import &imp)
{
if (!Kleo::userHasCertificationKey()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "No certification key available";
return false;
}
const char *fpr = imp.fingerprint();
if (!fpr) {
// WTF
qCWarning(KLEOPATRA_LOG) << "Import without fingerprint";
return false;
}
// Exactly one public key imported. Let's see if it is openpgp. We are async here so
// we can just fetch it.
auto ctx = wrap_unique(GpgME::Context::createForProtocol(GpgME::OpenPGP));
if (!ctx) {
// WTF
qCWarning(KLEOPATRA_LOG) << "Failed to create OpenPGP proto";
return false;
}
ctx->addKeyListMode(KeyListMode::WithSecret);
GpgME::Error err;
const auto key = ctx->key(fpr, err, false);
if (key.isNull() || err) {
// No such key most likely not OpenPGP
return false;
}
if (!Kleo::canBeCertified(key)) {
// key is expired or revoked
return false;
}
if (key.hasSecret()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "Secret key is available -> skipping certification";
return false;
}
for (const auto &uid : key.userIDs()) {
if (uid.validity() >= GpgME::UserID::Marginal) {
// Already marginal so don't bug the user
return false;
}
}
const QStringList suggestions = {
i18n("A phone call to the person."),
i18n("Using a business card."),
i18n("Confirming it on a trusted website."),
};
auto sel = KMessageBox::questionTwoActions(parentWidgetOrView(),
i18n("In order to mark the certificate as valid it needs to be certified.") + QStringLiteral("
")
+ i18n("Certifying means that you check the Fingerprint.") + QStringLiteral("
")
+ i18n("Some suggestions to do this are:")
+ QStringLiteral("%1").arg(suggestions.join(QStringLiteral("
") + i18n("Do you wish to start this process now?"),
i18nc("@title", "You have imported a new certificate (public key)"),
KGuiItem(i18nc("@action:button", "Certify")),
KStandardGuiItem::cancel(),
QStringLiteral("CertifyQuestion"));
if (sel == KMessageBox::ButtonCode::PrimaryAction) {
QEventLoop loop;
auto cmd = new Commands::CertifyCertificateCommand(key);
cmd->setParentWidget(parentWidgetOrView());
connect(cmd, &Command::finished, &loop, &QEventLoop::quit);
QMetaObject::invokeMethod(cmd, &Commands::CertifyCertificateCommand::start, Qt::QueuedConnection);
loop.exec();
}
return true;
}
namespace
{
/**
* Returns the Import of an OpenPGP key, if a single certificate was imported and this was an OpenPGP key.
* Otherwise, returns a null Import.
*/
auto getSingleOpenPGPImport(const std::vector &res)
{
static const Import nullImport;
if (!isImportFromSingleSource(res)) {
return nullImport;
}
const auto numImported = std::accumulate(res.cbegin(), res.cend(), 0, [](auto s, const auto &r) {
return s + r.result.numImported();
});
if (numImported > 1) {
return nullImport;
}
if ((res.size() >= 1) && (res[0].protocol == GpgME::OpenPGP) && (res[0].result.numImported() == 1) && (res[0].result.imports().size() == 1)) {
return res[0].result.imports()[0];
} else if ((res.size() == 2) && (res[1].protocol == GpgME::OpenPGP) && (res[1].result.numImported() == 1) && (res[1].result.imports().size() == 1)) {
return res[1].result.imports()[0];
}
return nullImport;
}
auto consolidatedAuditLogEntries(const std::vector &res)
{
static const QString gpg = QStringLiteral("gpg");
static const QString gpgsm = QStringLiteral("gpgsm");
if (res.size() == 1) {
return res.front().auditLog;
}
QStringList auditLogs;
auto extractAndAnnotateAuditLog = [](const ImportResultData &r) {
QString s;
if (!r.id.isEmpty()) {
const auto program = r.protocol == GpgME::OpenPGP ? gpg : gpgsm;
const auto headerLine = i18nc("file name (imported with gpg/gpgsm)", "%1 (imported with %2)").arg(r.id, program);
s += QStringLiteral("%1
").arg(headerLine);
}
if (r.auditLog.error().code() == GPG_ERR_NO_DATA) {
s += QStringLiteral("") + i18nc("@info", "Audit log is empty.") + QStringLiteral("");
} else if (r.result.error().isCanceled()) {
s += QStringLiteral("") + i18nc("@info", "Import was canceled.") + QStringLiteral("");
} else {
s += r.auditLog.text();
}
return s;
};
std::transform(res.cbegin(), res.cend(), std::back_inserter(auditLogs), extractAndAnnotateAuditLog);
return AuditLogEntry{auditLogs.join(QLatin1StringView{"
"}), Error{}};
}
}
void ImportCertificatesCommand::Private::showDetails(const std::vector &res, const std::vector &groups)
{
const auto singleOpenPGPImport = getSingleOpenPGPImport(res);
if (std::ranges::any_of(res, [](const auto &result) {
return result.result.numImported() > 0;
})) {
setImportResultProxyModel(res);
}
if (!singleOpenPGPImport.isNull()) {
if (showPleaseCertify(singleOpenPGPImport)) {
return;
}
}
MessageBox::information(parentWidgetOrView(), make_message_report(res, groups), consolidatedAuditLogEntries(res), i18n("Certificate Import Result"));
}
static QString make_error_message(const Error &err, const QString &id)
{
Q_ASSERT(err);
Q_ASSERT(!err.isCanceled());
if (id.isEmpty()) {
return i18n(
"An error occurred while trying to import the certificate:
"
"%1
",
Formatting::errorAsString(err));
} else {
return i18n(
"An error occurred while trying to import the certificate %1:
"
"%2
",
id,
Formatting::errorAsString(err));
}
}
void ImportCertificatesCommand::Private::showError(const ImportResultData &result)
{
MessageBox::error(parentWidgetOrView(), make_error_message(result.result.error(), result.id), result.auditLog);
}
void ImportCertificatesCommand::Private::setWaitForMoreJobs(bool wait)
{
if (wait == waitForMoreJobs) {
return;
}
waitForMoreJobs = wait;
if (!waitForMoreJobs) {
tryToFinish();
}
}
void ImportCertificatesCommand::Private::onImportResult(const ImportResult &result, QGpgME::Job *finishedJob)
{
if (!finishedJob) {
finishedJob = qobject_cast(q->sender());
}
Q_ASSERT(finishedJob);
qCDebug(KLEOPATRA_LOG) << q << __func__ << finishedJob;
auto it = std::find_if(std::begin(runningJobs), std::end(runningJobs), [finishedJob](const auto &job) {
return job.job == finishedJob;
});
Q_ASSERT(it != std::end(runningJobs));
if (it == std::end(runningJobs)) {
qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Finished job not found";
return;
}
Kleo::for_each(it->connections, &disconnectConnection);
it->connections.clear();
increaseProgressValue();
const auto job = *it;
addImportResult({job.id, job.protocol, job.type, result, AuditLogEntry::fromJob(finishedJob)}, job);
}
void ImportCertificatesCommand::Private::addImportResult(const ImportResultData &result, const ImportJobData &job)
{
qCDebug(KLEOPATRA_LOG) << q << __func__ << result.id << "Result:" << Formatting::errorAsString(result.result.error());
results.push_back(result);
if (importFailed(result)) {
showError(result);
}
if (job.job) {
const auto count = std::erase(runningJobs, job);
Q_ASSERT(count == 1);
}
tryToFinish();
}
static void handleOwnerTrust(const std::vector &results, QWidget *dialog)
{
std::unordered_set askedAboutFingerprints;
for (const auto &r : results) {
if (r.protocol != GpgME::Protocol::OpenPGP) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping non-OpenPGP import";
continue;
}
const auto imports = r.result.imports();
for (const auto &import : imports) {
if (!(import.status() & (Import::Status::NewKey | Import::Status::ContainedSecretKey))) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping already known imported public key";
continue;
}
const char *fpr = import.fingerprint();
if (!fpr) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping import without fingerprint";
continue;
}
if (Kleo::contains(askedAboutFingerprints, fpr)) {
// imports of secret keys can result in multiple Imports for the same key
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping import for already handled fingerprint";
continue;
}
GpgME::Error err;
auto ctx = wrap_unique(Context::createForProtocol(GpgME::Protocol::OpenPGP));
if (!ctx) {
qCWarning(KLEOPATRA_LOG) << "Failed to get context";
continue;
}
ctx->addKeyListMode(KeyListMode::WithSecret);
const Key toTrustOwner = ctx->key(fpr, err, false);
if (toTrustOwner.isNull() || !toTrustOwner.hasSecret()) {
continue;
}
if (toTrustOwner.ownerTrust() == Key::OwnerTrust::Ultimate) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping key with ultimate ownertrust";
continue;
}
const auto toTrustOwnerUserIDs{toTrustOwner.userIDs()};
// ki18n(" ") as initializer because initializing with empty string leads to
// (I18N_EMPTY_MESSAGE)
const KLocalizedString uids = std::accumulate(toTrustOwnerUserIDs.cbegin(),
toTrustOwnerUserIDs.cend(),
KLocalizedString{ki18n(" ")},
[](KLocalizedString temp, const auto &uid) {
return kxi18nc("@info", "%1- %2
").subs(temp).subs(Formatting::prettyNameAndEMail(uid));
});
const QString str = xi18nc("@info",
"You have imported a certificate with fingerprint"
"%1"
""
"and user IDs"
"%2
"
""
"Is this your own certificate?",
Formatting::prettyID(fpr),
uids);
int k = KMessageBox::questionTwoActionsCancel(dialog,
str,
i18nc("@title:window", "Mark Own Certificate"),
KGuiItem{i18nc("@action:button", "Yes, It's Mine")},
KGuiItem{i18nc("@action:button", "No, It's Not Mine")});
askedAboutFingerprints.insert(fpr);
if (k == KMessageBox::ButtonCode::PrimaryAction) {
// To use the ChangeOwnerTrustJob over
// the CryptoBackendFactory
const QGpgME::Protocol *const backend = QGpgME::openpgp();
if (!backend) {
qCWarning(KLEOPATRA_LOG) << "Failed to get CryptoBackend";
return;
}
ChangeOwnerTrustJob *const j = backend->changeOwnerTrustJob();
j->start(toTrustOwner, Key::Ultimate);
} else if (k == KMessageBox::ButtonCode::Cancel) {
// do not bother the user with further "Is this yours?" questions
return;
}
}
}
}
static void validateImportedCertificate(const GpgME::Import &import)
{
if (const auto fpr = import.fingerprint()) {
auto key = KeyCache::instance()->findByFingerprint(fpr);
if (!key.isNull()) {
// this triggers a keylisting with validation for this certificate
key.update();
} else {
qCWarning(KLEOPATRA_LOG) << __func__ << "Certificate with fingerprint" << fpr << "not found";
}
}
}
static void handleExternalCMSImports(const std::vector &results)
{
// For external CMS Imports we have to manually do a keylist
// with validation to get the intermediate and root ca imported
// automatically if trusted-certs and extra-certs are used.
for (const auto &r : results) {
if (r.protocol == GpgME::CMS && r.type == ImportType::External && !importFailed(r) && !importWasCanceled(r)) {
const auto imports = r.result.imports();
std::for_each(std::begin(imports), std::end(imports), &validateImportedCertificate);
}
}
}
void ImportCertificatesCommand::Private::processResults()
{
importGroups();
if (Settings{}.retrieveSignerKeysAfterImport() && !importingSignerKeys) {
importingSignerKeys = true;
const auto missingSignerKeys = getMissingSignerKeyIds(results);
if (!missingSignerKeys.empty()) {
importSignerKeys(missingSignerKeys);
return;
}
}
handleExternalCMSImports(results);
// ensure that the progress dialog is closed before we show any other dialogs
setProgressToMaximum();
handleOwnerTrust(results, parentWidgetOrView());
auto hasError = std::ranges::any_of(results, [](const auto &result) {
return importFailed(result);
});
auto allAreIrrelevant = std::ranges::all_of(results, [](const auto &result) {
return result.result.numConsidered() == 0 || (importFailed(result) && result.result.numConsidered() == 1);
});
if (!(hasError && allAreIrrelevant)) {
showDetails(results, importedGroups);
}
auto tv = dynamic_cast(view());
if (!tv) {
qCDebug(KLEOPATRA_LOG) << "Failed to find treeview";
} else {
tv->expandAll();
}
finished();
}
void ImportCertificatesCommand::Private::tryToFinish()
{
qCDebug(KLEOPATRA_LOG) << q << __func__;
if (waitForMoreJobs) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "Waiting for more jobs -> keep going";
return;
}
if (!runningJobs.empty()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are unfinished jobs -> keep going";
return;
}
if (!pendingJobs.empty()) {
qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are pending jobs -> start the next one";
auto job = pendingJobs.front();
pendingJobs.pop();
job.job->startNow();
runningJobs.push_back(job);
return;
}
if (keyListConnection) {
qCWarning(KLEOPATRA_LOG) << q << __func__ << "There is already a valid keyListConnection!";
} else {
auto keyCache = KeyCache::mutableInstance();
keyListConnection = connect(keyCache.get(), &KeyCache::keyListingDone, q, [this]() {
keyCacheUpdated();
});
keyCache->startKeyListing();
}
}
void ImportCertificatesCommand::Private::keyCacheUpdated()
{
qCDebug(KLEOPATRA_LOG) << q << __func__;
if (!disconnect(keyListConnection)) {
qCWarning(KLEOPATRA_LOG) << q << __func__ << "Failed to disconnect keyListConnection";
}
keyCacheAutoRefreshSuspension.reset();
const auto allIds = std::accumulate(std::cbegin(results), std::cend(results), std::set{}, [](auto allIds, const auto &r) {
allIds.insert(r.id);
return allIds;
});
const auto canceledIds = std::accumulate(std::cbegin(results), std::cend(results), std::set{}, [](auto canceledIds, const auto &r) {
if (importWasCanceled(r)) {
canceledIds.insert(r.id);
}
return canceledIds;
});
const auto totalConsidered = std::accumulate(std::cbegin(results), std::cend(results), 0, [](auto totalConsidered, const auto &r) {
return totalConsidered + r.result.numConsidered();
});
if (totalConsidered == 0 && canceledIds.size() == allIds.size()) {
// nothing was considered for import and at least one import per id was
// canceled => treat the command as canceled
canceled();
return;
}
processResults();
}
static ImportedGroup storeGroup(const KeyGroup &group, const QString &id, QWidget *parent)
{
- if (Kleo::any_of(group.keys(), [](const auto &key) {
+ if (std::ranges::any_of(group.keys(), [](const auto &key) {
return !Kleo::keyHasEncrypt(key);
})) {
KMessageBox::information(parent,
xi18nc("@info",
"The imported group%1contains certificates that cannot be used for encryption. "
"This may lead to unexpected results.",
group.name()));
}
const auto status = KeyCache::instance()->group(group.id()).isNull() ? ImportedGroup::Status::New : ImportedGroup::Status::Updated;
if (status == ImportedGroup::Status::New) {
KeyCache::mutableInstance()->insert(group);
} else {
KeyCache::mutableInstance()->update(group);
}
return {id, group, status};
}
void ImportCertificatesCommand::Private::importGroups()
{
for (const auto &path : filesToImportGroupsFrom) {
const bool certificateImportSucceeded = std::any_of(std::cbegin(results), std::cend(results), [path](const auto &r) {
return r.id == path && !importFailed(r) && !importWasCanceled(r);
});
if (certificateImportSucceeded) {
qCDebug(KLEOPATRA_LOG) << __func__ << "Importing groups from file" << path;
const auto groups = readKeyGroups(path);
std::transform(std::begin(groups), std::end(groups), std::back_inserter(importedGroups), [path, this](const auto &group) {
return storeGroup(group, path, parentWidgetOrView());
});
}
increaseProgressValue();
}
filesToImportGroupsFrom.clear();
}
static auto accumulateNewKeys(std::vector &fingerprints, const std::vector &imports)
{
return std::accumulate(std::begin(imports), std::end(imports), fingerprints, [](auto fingerprints, const auto &import) {
if (import.status() == Import::NewKey) {
fingerprints.push_back(import.fingerprint());
}
return fingerprints;
});
}
static auto accumulateNewOpenPGPKeys(const std::vector &results)
{
return std::accumulate(std::begin(results), std::end(results), std::vector{}, [](auto fingerprints, const auto &r) {
if (r.protocol == GpgME::OpenPGP) {
fingerprints = accumulateNewKeys(fingerprints, r.result.imports());
}
return fingerprints;
});
}
std::set ImportCertificatesCommand::Private::getMissingSignerKeyIds(const std::vector &results)
{
auto newOpenPGPKeys = KeyCache::instance()->findByFingerprint(accumulateNewOpenPGPKeys(results));
// update all new OpenPGP keys to get information about certifications
std::for_each(std::begin(newOpenPGPKeys), std::end(newOpenPGPKeys), std::mem_fn(&Key::update));
auto missingSignerKeyIds = Kleo::getMissingSignerKeyIds(newOpenPGPKeys);
return missingSignerKeyIds;
}
void ImportCertificatesCommand::Private::importSignerKeys(const std::set &keyIds)
{
Q_ASSERT(!keyIds.empty());
setProgressLabelText(i18np("Fetching 1 signer key... (this can take a while)", "Fetching %1 signer keys... (this can take a while)", keyIds.size()));
setWaitForMoreJobs(true);
// start one import per key id to allow canceling the key retrieval without
// losing already retrieved keys
for (const auto &keyId : keyIds) {
startImport(GpgME::OpenPGP, {keyId}, QStringLiteral("Retrieve Signer Keys"));
}
setWaitForMoreJobs(false);
}
static std::unique_ptr get_import_job(GpgME::Protocol protocol)
{
Q_ASSERT(protocol != UnknownProtocol);
if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
return std::unique_ptr(backend->importJob());
} else {
return std::unique_ptr();
}
}
void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol,
const QByteArray &data,
const QString &id,
[[maybe_unused]] const ImportOptions &options)
{
Q_ASSERT(protocol != UnknownProtocol);
if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) {
return;
}
std::unique_ptr job = get_import_job(protocol);
if (!job.get()) {
nonWorkingProtocols.push_back(protocol);
error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)),
i18n("Certificate Import Failed"));
addImportResult({id, protocol, ImportType::Local, ImportResult{}, AuditLogEntry{}});
return;
}
keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
std::vector connections = {
connect(job.get(),
&AbstractImportJob::result,
q,
[this](const GpgME::ImportResult &result) {
onImportResult(result);
}),
connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
};
job->setImportFilter(options.importFilter);
job->setKeyOrigin(options.keyOrigin, options.keyOriginUrl);
const GpgME::Error err = job->startLater(data);
if (err.code()) {
addImportResult({id, protocol, ImportType::Local, ImportResult{err}, AuditLogEntry{}});
} else {
increaseProgressMaximum();
pendingJobs.push({id, protocol, ImportType::Local, job.release(), connections});
}
}
static std::unique_ptr get_import_from_keyserver_job(GpgME::Protocol protocol)
{
Q_ASSERT(protocol != UnknownProtocol);
if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
return std::unique_ptr(backend->importFromKeyserverJob());
} else {
return std::unique_ptr();
}
}
void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, const std::vector &keys, const QString &id)
{
Q_ASSERT(protocol != UnknownProtocol);
if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) {
return;
}
std::unique_ptr job = get_import_from_keyserver_job(protocol);
if (!job.get()) {
nonWorkingProtocols.push_back(protocol);
error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)),
i18n("Certificate Import Failed"));
addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}});
return;
}
keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
std::vector connections = {
connect(job.get(),
&AbstractImportJob::result,
q,
[this](const GpgME::ImportResult &result) {
onImportResult(result);
}),
connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
};
const GpgME::Error err = job->start(keys);
if (err.code()) {
addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}});
} else {
increaseProgressMaximum();
runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections});
}
}
static auto get_receive_keys_job(GpgME::Protocol protocol)
{
Q_ASSERT(protocol != UnknownProtocol);
std::unique_ptr job{};
if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) {
job.reset(backend->receiveKeysJob());
}
return job;
}
void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, [[maybe_unused]] const QStringList &keyIds, const QString &id)
{
Q_ASSERT(protocol != UnknownProtocol);
auto job = get_receive_keys_job(protocol);
if (!job.get()) {
qCWarning(KLEOPATRA_LOG) << "Failed to get ReceiveKeysJob for protocol" << Formatting::displayName(protocol);
addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}});
return;
}
keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh();
std::vector connections = {
connect(job.get(),
&AbstractImportJob::result,
q,
[this](const GpgME::ImportResult &result) {
onImportResult(result);
}),
connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress),
};
const GpgME::Error err = job->start(keyIds);
if (err.code()) {
addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}});
} else {
increaseProgressMaximum();
runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections});
}
}
void ImportCertificatesCommand::Private::importGroupsFromFile(const QString &filename)
{
increaseProgressMaximum();
filesToImportGroupsFrom.push_back(filename);
}
void ImportCertificatesCommand::Private::setUpProgressDialog()
{
if (progressDialog) {
return;
}
progressDialog = new QProgressDialog{parentWidgetOrView()};
// use a non-modal progress dialog to avoid reentrancy problems (and crashes) if multiple jobs finish in the same event loop cycle
// (cf. the warning for QProgressDialog::setValue() in the API documentation)
progressDialog->setModal(false);
progressDialog->setWindowTitle(progressWindowTitle);
progressDialog->setLabelText(progressLabelText);
progressDialog->setMinimumDuration(1000);
progressDialog->setMaximum(1);
progressDialog->setValue(0);
connect(progressDialog, &QProgressDialog::canceled, q, &Command::cancel);
connect(q, &Command::finished, progressDialog, [this]() {
progressDialog->accept();
});
}
void ImportCertificatesCommand::Private::setProgressWindowTitle(const QString &title)
{
if (progressDialog) {
progressDialog->setWindowTitle(title);
} else {
progressWindowTitle = title;
}
}
void ImportCertificatesCommand::Private::setProgressLabelText(const QString &text)
{
if (progressDialog) {
progressDialog->setLabelText(text);
} else {
progressLabelText = text;
}
}
void ImportCertificatesCommand::Private::increaseProgressMaximum()
{
setUpProgressDialog();
progressDialog->setMaximum(progressDialog->maximum() + 1);
qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum();
}
void ImportCertificatesCommand::Private::increaseProgressValue()
{
progressDialog->setValue(progressDialog->value() + 1);
qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum();
}
void ImportCertificatesCommand::Private::setProgressToMaximum()
{
qCDebug(KLEOPATRA_LOG) << __func__;
progressDialog->setValue(progressDialog->maximum());
}
void ImportCertificatesCommand::doCancel()
{
const auto jobsToCancel = d->runningJobs;
std::for_each(std::begin(jobsToCancel), std::end(jobsToCancel), [this](const auto &job) {
if (!job.connections.empty()) {
// ignore jobs without connections; they are already completed
qCDebug(KLEOPATRA_LOG) << "Canceling job" << job.job;
job.job->slotCancel();
d->onImportResult(ImportResult{Error::fromCode(GPG_ERR_CANCELED)}, job.job);
}
});
}
#undef d
#undef q
#include "importcertificatescommand.moc"
#include "moc_importcertificatescommand.cpp"
diff --git a/src/commands/keytocardcommand.cpp b/src/commands/keytocardcommand.cpp
index 298b97bc2..66b4005c2 100644
--- a/src/commands/keytocardcommand.cpp
+++ b/src/commands/keytocardcommand.cpp
@@ -1,782 +1,782 @@
/* commands/keytocardcommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik
SPDX-FileContributor: Intevation GmbH
SPDX-FileCopyrightText: 2020,2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "keytocardcommand.h"
#include "cardcommand_p.h"
#include "authenticatepivcardapplicationcommand.h"
#include "smartcard/algorithminfo.h"
#include "smartcard/openpgpcard.h"
#include "smartcard/pivcard.h"
#include "smartcard/readerstatus.h"
#include "smartcard/utils.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36
#define GPG_ERROR_HAS_NO_AUTH
#endif
#include "kleopatra_debug.h"
using namespace Kleo;
using namespace Kleo::Commands;
using namespace Kleo::SmartCard;
using namespace GpgME;
namespace
{
QString cardDisplayName(const std::shared_ptr &card)
{
return i18nc("smartcard application - serial number of smartcard", "%1 - %2", displayAppName(card->appName()), card->displaySerialNumber());
}
}
class KeyToCardCommand::Private : public CardCommand::Private
{
friend class ::Kleo::Commands::KeyToCardCommand;
KeyToCardCommand *q_func() const
{
return static_cast(q);
}
public:
explicit Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey);
explicit Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName);
private:
enum Confirmation {
AskForConfirmation,
SkipConfirmation,
};
void start();
void startKeyToOpenPGPCard();
Subkey getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &card);
void startKeyToPIVCard();
void authenticate();
void authenticationFinished();
void authenticationCanceled();
void keyToCardDone(const GpgME::Error &err);
void keyToPIVCardDone(const GpgME::Error &err);
void updateDone();
void keyHasBeenCopiedToCard();
void backupHasBeenCreated(const QString &backupFilename);
QString backupKey();
std::vector readSecretKeyFile();
bool writeSecretKeyBackup(const QString &filename, const std::vector &keydata);
void startDeleteSecretKeyLocally(Confirmation confirmation);
void deleteSecretKeyLocallyFinished(const GpgME::Error &err);
private:
std::string appName;
GpgME::Subkey subkey;
std::string cardSlot;
bool overwriteExistingAlreadyApproved = false;
bool hasBeenCanceled = false;
QMetaObject::Connection updateConnection;
};
KeyToCardCommand::Private *KeyToCardCommand::d_func()
{
return static_cast(d.get());
}
const KeyToCardCommand::Private *KeyToCardCommand::d_func() const
{
return static_cast(d.get());
}
#define q q_func()
#define d d_func()
KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey_)
: CardCommand::Private(qq, "", nullptr)
, subkey(subkey_)
{
}
KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName_)
: CardCommand::Private(qq, serialNumber, nullptr)
, appName(appName_)
, cardSlot(slot)
{
}
namespace
{
static std::shared_ptr getCardToTransferSubkeyTo(const Subkey &subkey, QWidget *parent)
{
const std::vector> suitableCards = KeyToCardCommand::getSuitableCards(subkey);
if (suitableCards.empty()) {
return std::shared_ptr();
} else if (suitableCards.size() == 1) {
return suitableCards[0];
}
QStringList options;
for (const auto &card : suitableCards) {
options.push_back(cardDisplayName(card));
}
bool ok;
const QString choice = QInputDialog::getItem(parent,
i18n("Select Card"),
i18n("Please select the card the key should be written to:"),
options,
/* current= */ 0,
/* editable= */ false,
&ok);
if (!ok) {
return std::shared_ptr();
}
const int index = options.indexOf(choice);
return suitableCards[index];
}
}
void KeyToCardCommand::Private::start()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::start()";
if (!subkey.isNull() && serialNumber().empty()) {
const auto card = getCardToTransferSubkeyTo(subkey, parentWidgetOrView());
if (!card) {
finished();
return;
}
setSerialNumber(card->serialNumber());
appName = card->appName();
}
const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName);
if (!card) {
error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber())));
finished();
return;
}
if (card->appName() == SmartCard::OpenPGPCard::AppName) {
startKeyToOpenPGPCard();
} else if (card->appName() == SmartCard::PIVCard::AppName) {
startKeyToPIVCard();
} else {
error(xi18nc("@info", "Sorry! Writing keys to the card %1 is not supported.", cardDisplayName(card)));
finished();
return;
}
}
namespace
{
static std::string getOpenPGPCardSlotForKey(const GpgME::Subkey &subKey, QWidget *parent)
{
// Check if we need to ask the user for the slot
if ((subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt() && !subKey.canAuthenticate()) {
// Signing only
return OpenPGPCard::pgpSigKeyRef();
}
if (subKey.canEncrypt() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canAuthenticate()) {
// Encrypt only
return OpenPGPCard::pgpEncKeyRef();
}
if (subKey.canAuthenticate() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt()) {
// Auth only
return OpenPGPCard::pgpAuthKeyRef();
}
// Multiple uses, ask user.
QStringList options;
std::vector cardSlots;
if (subKey.canSign() || subKey.canCertify()) {
options.push_back(i18nc("@item:inlistbox", "Signature"));
cardSlots.push_back(OpenPGPCard::pgpSigKeyRef());
}
if (subKey.canEncrypt()) {
options.push_back(i18nc("@item:inlistbox", "Encryption"));
cardSlots.push_back(OpenPGPCard::pgpEncKeyRef());
}
if (subKey.canAuthenticate()) {
options.push_back(i18nc("@item:inlistbox", "Authentication"));
cardSlots.push_back(OpenPGPCard::pgpAuthKeyRef());
}
bool ok;
const QString choice = QInputDialog::getItem(parent,
i18n("Select Card Slot"),
i18n("Please select the card slot the key should be written to:"),
options,
/* current= */ 0,
/* editable= */ false,
&ok);
const int choiceIndex = options.indexOf(choice);
if (ok && choiceIndex >= 0) {
return cardSlots[choiceIndex];
} else {
return {};
}
}
}
void KeyToCardCommand::Private::startKeyToOpenPGPCard()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToOpenPGPCard()";
const auto pgpCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber());
if (!pgpCard) {
error(i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(serialNumber())));
finished();
return;
}
if (subkey.isNull()) {
finished();
return;
}
if (subkey.parent().protocol() != GpgME::OpenPGP) {
error(i18n("Sorry! This key cannot be transferred to an OpenPGP card."));
finished();
return;
}
cardSlot = getOpenPGPCardSlotForKey(subkey, parentWidgetOrView());
if (cardSlot.empty()) {
finished();
return;
}
// Check if we need to do the overwrite warning.
const std::string existingKey = pgpCard->keyFingerprint(cardSlot);
if (!existingKey.empty()) {
const auto encKeyWarning = (cardSlot == OpenPGPCard::pgpEncKeyRef())
? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.")
: QString{};
const QString message = i18nc("@info",
"The card %1 already contains a key in this slot. Continuing will overwrite that key.
"
"If there is no backup the existing key will be irrecoverably lost.
",
cardDisplayName(pgpCard))
+ i18n("The existing key has the fingerprint:") + QStringLiteral("%1
").arg(Formatting::prettyID(existingKey.c_str())) + encKeyWarning;
const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(),
message,
i18nc("@title:window", "Overwrite existing key"),
KGuiItem{i18nc("@action:button", "Overwrite Existing Key")},
KStandardGuiItem::cancel(),
QString(),
KMessageBox::Notify | KMessageBox::Dangerous);
if (choice != KMessageBox::Continue) {
finished();
return;
}
}
// Now do the deed
const auto time = QDateTime::fromSecsSinceEpoch(quint32(subkey.creationTime()), QTimeZone::utc());
const auto timestamp = time.toString(QStringLiteral("yyyyMMdd'T'HHmmss"));
const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 %3 %4")
.arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber()), QString::fromStdString(cardSlot), timestamp);
ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) {
keyToCardDone(err);
});
}
namespace
{
static std::vector getSigningCertificates()
{
std::vector signingCertificates = KeyCache::instance()->secretKeys();
const auto it = std::remove_if(signingCertificates.begin(), signingCertificates.end(), [](const Key &key) {
return !(key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canSign() && !key.subkey(0).canEncrypt() && key.subkey(0).isSecret()
&& !key.subkey(0).isCardKey());
});
signingCertificates.erase(it, signingCertificates.end());
return signingCertificates;
}
static std::vector getEncryptionCertificates()
{
std::vector encryptionCertificates = KeyCache::instance()->secretKeys();
const auto it = std::remove_if(encryptionCertificates.begin(), encryptionCertificates.end(), [](const Key &key) {
return !(key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canEncrypt() && key.subkey(0).isSecret()
&& !key.subkey(0).isCardKey());
});
encryptionCertificates.erase(it, encryptionCertificates.end());
return encryptionCertificates;
}
}
Subkey KeyToCardCommand::Private::getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr & /*card*/)
{
if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) {
return Subkey();
}
const std::vector certificates = cardSlot == PIVCard::cardAuthenticationKeyRef() ? getSigningCertificates() : getEncryptionCertificates();
if (certificates.empty()) {
error(i18n("Sorry! No suitable certificate to write to this card slot was found."));
return Subkey();
}
auto dialog = new KeySelectionDialog(parentWidgetOrView());
dialog->setWindowTitle(i18nc("@title:window", "Select Certificate"));
dialog->setText(i18n("Please select the certificate whose key pair you want to write to the card:"));
dialog->setKeys(certificates);
if (dialog->exec() == QDialog::Rejected) {
return Subkey();
}
return dialog->selectedKey().subkey(0);
}
void KeyToCardCommand::Private::startKeyToPIVCard()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToPIVCard()";
const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber());
if (!pivCard) {
error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber())));
finished();
return;
}
if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) {
// key to card is only supported for the Card Authentication key and the Key Management key
finished();
return;
}
if (subkey.isNull()) {
subkey = getSubkeyToTransferToPIVCard(cardSlot, pivCard);
}
if (subkey.isNull()) {
finished();
return;
}
if (subkey.parent().protocol() != GpgME::CMS) {
error(i18n("Sorry! This key cannot be transferred to a PIV card."));
finished();
return;
}
if (!subkey.canEncrypt() && !subkey.canSign()) {
error(i18n("Sorry! Only encryption keys and signing keys can be transferred to a PIV card."));
finished();
return;
}
// Check if we need to do the overwrite warning.
if (!overwriteExistingAlreadyApproved) {
const std::string existingKey = pivCard->keyInfo(cardSlot).grip;
if (!existingKey.empty() && (existingKey != subkey.keyGrip())) {
const QString decryptionWarning = (cardSlot == PIVCard::keyManagementKeyRef())
? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.")
: QString();
const QString message = i18nc("@info",
"The card %1 already contains a key in this slot. Continuing will overwrite that key.
"
"If there is no backup the existing key will be irrecoverably lost.
",
cardDisplayName(pivCard))
+ i18n("The existing key has the key grip:") + QStringLiteral("%1
").arg(QString::fromStdString(existingKey)) + decryptionWarning;
const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(),
message,
i18nc("@title:window", "Overwrite existing key"),
KGuiItem{i18nc("@action:button", "Overwrite Existing Key")},
KStandardGuiItem::cancel(),
QString(),
KMessageBox::Notify | KMessageBox::Dangerous);
if (choice != KMessageBox::Continue) {
finished();
return;
}
overwriteExistingAlreadyApproved = true;
}
}
const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 %3")
.arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber()))
.arg(QString::fromStdString(cardSlot));
ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) {
keyToPIVCardDone(err);
});
}
void KeyToCardCommand::Private::authenticate()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticate()";
auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView());
cmd->setAutoResetCardToOpenPGP(false);
connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() {
authenticationFinished();
});
connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() {
authenticationCanceled();
});
cmd->start();
}
void KeyToCardCommand::Private::authenticationFinished()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationFinished()";
if (!hasBeenCanceled) {
startKeyToPIVCard();
}
}
void KeyToCardCommand::Private::authenticationCanceled()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationCanceled()";
hasBeenCanceled = true;
canceled();
}
void KeyToCardCommand::Private::updateDone()
{
disconnect(updateConnection);
const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName);
if (!card) {
error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber())));
finished();
return;
}
const std::string keyGripOnCard = card->keyInfo(cardSlot).grip;
if (keyGripOnCard != subkey.keyGrip()) {
qCWarning(KLEOPATRA_LOG) << q << __func__ << "KEYTOCARD succeeded, but key on card doesn't match copied key";
error(i18nc("@info", "Copying the key to the card failed."));
finished();
return;
}
keyHasBeenCopiedToCard();
}
void KeyToCardCommand::Private::keyHasBeenCopiedToCard()
{
const auto answer = KMessageBox::questionTwoActionsCancel(parentWidgetOrView(),
xi18nc("@info",
"The key has been copied to the card."
"You can now delete the copy of the key stored on this computer. "
"Optionally, you can first create a backup of the key."),
i18nc("@title:window", "Success"),
KGuiItem{i18nc("@action:button", "Create backup")},
KGuiItem{i18nc("@action:button", "Delete copy on disk")},
KGuiItem{i18nc("@action:button", "Keep copy on disk")});
if (answer == KMessageBox::ButtonCode::PrimaryAction) {
const QString backupFilename = backupKey();
if (backupFilename.isEmpty()) {
// user canceled the backup or there was an error; repeat above question
QMetaObject::invokeMethod(q, [this]() {
keyHasBeenCopiedToCard();
});
}
backupHasBeenCreated(backupFilename);
} else if (answer == KMessageBox::ButtonCode::SecondaryAction) {
startDeleteSecretKeyLocally(AskForConfirmation);
} else {
finished();
}
}
void KeyToCardCommand::Private::backupHasBeenCreated(const QString &backupFilename)
{
const auto answer =
KMessageBox::questionTwoActions(parentWidgetOrView(),
xi18nc("@info",
"The key has been copied to the card and a backup has been written to %1."
"Do you want to delete the copy of the key stored on this computer?",
backupFilename),
i18nc("@title:window", "Success"),
KGuiItem{i18nc("@action:button", "Delete copy on disk")},
KGuiItem{i18nc("@action:button", "Keep copy on disk")});
if (answer == KMessageBox::ButtonCode::PrimaryAction) {
// the user has created a backup; don't ask again for confirmation before deleting the copy on disk
startDeleteSecretKeyLocally(SkipConfirmation);
} else {
finished();
}
}
namespace
{
QString gnupgPrivateKeyBackupExtension()
{
return QStringLiteral(".gpgsk");
}
QString proposeFilename(const Subkey &subkey)
{
QString filename;
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(QLatin1StringView{", "}, QLatin1String{"_"});
/* Not translated so it's better to use in tutorials etc. */
filename = ((shortKeyID == shortSubkeyID) //
? QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3"}.arg(name, shortKeyID, usage)
: QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3_%4"}.arg(name, shortKeyID, shortSubkeyID, usage));
filename.replace(u'/', u'_');
return QDir{ApplicationState::lastUsedExportDirectory()}.filePath(filename + gnupgPrivateKeyBackupExtension());
}
QString requestPrivateKeyBackupFilename(const QString &proposedFilename, QWidget *parent)
{
auto filename = FileDialog::getSaveFileNameEx(parent,
i18nc("@title:window", "Backup Secret Key"),
QStringLiteral("imp"),
proposedFilename,
i18nc("description of filename filter", "Secret Key Backup Files") + QLatin1StringView{" (*.gpgsk)"});
if (!filename.isEmpty()) {
const QFileInfo fi{filename};
if (fi.suffix().isEmpty()) {
filename += gnupgPrivateKeyBackupExtension();
}
ApplicationState::setLastUsedExportDirectory(filename);
}
return filename;
}
}
QString KeyToCardCommand::Private::backupKey()
{
static const QByteArray backupInfoName = "Backup-info:";
auto keydata = readSecretKeyFile();
if (keydata.empty()) {
return {};
}
const auto filename = requestPrivateKeyBackupFilename(proposeFilename(subkey), parentWidgetOrView());
if (filename.isEmpty()) {
return {};
}
// remove old backup info
Kleo::erase_if(keydata, [](const auto &line) {
return line.startsWith(backupInfoName);
});
// prepend new backup info
const QByteArrayList backupInfo = {
backupInfoName,
subkey.keyGrip(),
QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toUtf8(),
"Kleopatra",
Formatting::prettyNameAndEMail(subkey.parent()).toUtf8(),
};
keydata.insert(keydata.begin(), backupInfo.join(' ') + '\n');
if (writeSecretKeyBackup(filename, keydata)) {
return filename;
} else {
return {};
}
}
std::vector KeyToCardCommand::Private::readSecretKeyFile()
{
const auto filename = QString::fromLatin1(subkey.keyGrip()) + QLatin1StringView{".key"};
const auto path = QDir{Kleo::gnupgPrivateKeysDirectory()}.filePath(filename);
QFile file{path};
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
error(xi18n("Cannot open the private key file %1 for reading.", path));
return {};
}
std::vector lines;
while (!file.atEnd()) {
lines.push_back(file.readLine());
}
if (lines.empty()) {
error(xi18n("The private key file %1 is empty.", path));
}
return lines;
}
bool KeyToCardCommand::Private::writeSecretKeyBackup(const QString &filename, const std::vector &keydata)
{
QSaveFile file{filename};
// open the file in binary format because we want to write Unix line endings
if (!file.open(QIODevice::WriteOnly)) {
error(xi18n("Cannot open the file %1 for writing.", filename));
return false;
}
for (const auto &line : keydata) {
file.write(line);
}
if (!file.commit()) {
error(xi18n("Writing the backup of the secret key to %1 failed.", filename));
return false;
};
return true;
}
void KeyToCardCommand::Private::startDeleteSecretKeyLocally(Confirmation confirmation)
{
const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName);
if (!card) {
error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber())));
finished();
return;
}
if (confirmation == AskForConfirmation) {
const auto answer = KMessageBox::questionTwoActions(parentWidgetOrView(),
xi18nc("@info", "Do you really want to delete the copy of the key stored on this computer?"),
i18nc("@title:window", "Confirm Deletion"),
KStandardGuiItem::del(),
KStandardGuiItem::cancel(),
{},
KMessageBox::Notify | KMessageBox::Dangerous);
if (answer != KMessageBox::ButtonCode::PrimaryAction) {
finished();
return;
}
}
const auto cmd = QByteArray{"DELETE_KEY --force "} + subkey.keyGrip();
ReaderStatus::mutableInstance()->startSimpleTransaction(card, cmd, q, [this](const GpgME::Error &err) {
deleteSecretKeyLocallyFinished(err);
});
}
void KeyToCardCommand::Private::deleteSecretKeyLocallyFinished(const GpgME::Error &err)
{
if (err) {
error(xi18nc("@info",
"Failed to delete the copy of the key stored on this computer:%1",
Formatting::errorAsString(err)));
}
ReaderStatus::mutableInstance()->updateStatus();
success(i18nc("@info", "Successfully deleted the copy of the key stored on this computer."));
finished();
}
KeyToCardCommand::KeyToCardCommand(const GpgME::Subkey &subkey)
: CardCommand(new Private(this, subkey))
{
}
KeyToCardCommand::KeyToCardCommand(const std::string &cardSlot, const std::string &serialNumber, const std::string &appName)
: CardCommand(new Private(this, cardSlot, serialNumber, appName))
{
}
KeyToCardCommand::~KeyToCardCommand()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::~KeyToCardCommand()";
}
namespace
{
bool cardSupportsKeyAlgorithm(const std::shared_ptr &card, const std::string &keyAlgo)
{
if (card->appName() == OpenPGPCard::AppName) {
const auto pgpCard = static_cast(card.get());
const auto cardAlgos = pgpCard->supportedAlgorithms();
- return Kleo::any_of(cardAlgos, [keyAlgo](const auto &algo) {
+ return std::ranges::any_of(cardAlgos, [keyAlgo](const auto &algo) {
return (keyAlgo == algo.id) //
|| (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpEncKeyRef()))
|| (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpSigKeyRef()));
});
}
return false;
}
}
// static
std::vector> KeyToCardCommand::getSuitableCards(const GpgME::Subkey &subkey)
{
std::vector> suitableCards;
if (subkey.isNull() || subkey.parent().protocol() != GpgME::OpenPGP) {
return suitableCards;
}
const auto keyAlgo = subkey.algoName();
Kleo::copy_if(ReaderStatus::instance()->getCards(), std::back_inserter(suitableCards), [keyAlgo](const auto &card) {
return cardSupportsKeyAlgorithm(card, keyAlgo);
});
return suitableCards;
}
void KeyToCardCommand::Private::keyToCardDone(const GpgME::Error &err)
{
if (!err && !err.isCanceled()) {
updateConnection = connect(ReaderStatus::instance(), &ReaderStatus::updateFinished, q, [this]() {
updateDone();
});
ReaderStatus::mutableInstance()->updateCard(serialNumber(), appName);
return;
}
if (err) {
error(xi18nc("@info", "Copying the key to the card failed:%1", Formatting::errorAsString(err)));
}
finished();
}
void KeyToCardCommand::Private::keyToPIVCardDone(const GpgME::Error &err)
{
qCDebug(KLEOPATRA_LOG) << q << __func__ << Formatting::errorAsString(err) << "(" << err.code() << ")";
#ifdef GPG_ERROR_HAS_NO_AUTH
// gpgme 1.13 reports "BAD PIN" instead of "NO AUTH"
if (err.code() == GPG_ERR_NO_AUTH || err.code() == GPG_ERR_BAD_PIN) {
authenticate();
return;
}
#endif
keyToCardDone(err);
}
void KeyToCardCommand::doStart()
{
qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::doStart()";
d->start();
}
void KeyToCardCommand::doCancel()
{
}
#undef q_func
#undef d_func
#include "moc_keytocardcommand.cpp"
diff --git a/src/commands/revokecertificationcommand.cpp b/src/commands/revokecertificationcommand.cpp
index f14612eeb..070345c38 100644
--- a/src/commands/revokecertificationcommand.cpp
+++ b/src/commands/revokecertificationcommand.cpp
@@ -1,370 +1,370 @@
/* -*- mode: c++; c-basic-offset:4 -*-
commands/revokecertificationcommand.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2020, 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "revokecertificationcommand.h"
#include "command_p.h"
#include "exportopenpgpcertstoservercommand.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace Kleo;
using namespace Kleo::Commands;
using namespace GpgME;
using namespace QGpgME;
namespace
{
enum class InputType {
Key,
UserIDs,
Certifications,
};
struct CertificationData {
UserID userId;
Key certificationKey;
UserID::Signature signature;
};
struct KeyAndSignature {
Key key;
UserID::Signature signature;
};
static std::vector getCertificationKeys(const GpgME::UserID &userId)
{
std::vector keys;
if (userId.numSignatures() == 0) {
qCWarning(KLEOPATRA_LOG) << __func__ << "- Error: Signatures of user ID" << QString::fromUtf8(userId.id()) << "not available";
return keys;
}
std::vector revokableCertifications;
Kleo::copy_if(userId.signatures(), std::back_inserter(revokableCertifications), [](const auto &certification) {
return userCanRevokeCertification(certification) == CertificationCanBeRevoked;
});
Kleo::transform(revokableCertifications, std::back_inserter(keys), [](const auto &certification) {
return KeyAndSignature{KeyCache::instance()->findByKeyIDOrFingerprint(certification.signerKeyID()), certification};
});
return keys;
}
static bool confirmRevocations(QWidget *parent, const std::vector &certifications)
{
KMessageBox::ButtonCode answer;
if (certifications.size() == 1) {
const auto [userId, certificationKey, signature] = certifications.front();
const auto message = xi18nc("@info",
"You are about to revoke the certification of user ID%1made with the key%2.",
QString::fromUtf8(userId.id()),
Formatting::formatForComboBox(certificationKey));
answer = KMessageBox::questionTwoActions(parent,
message,
i18nc("@title:window", "Confirm Revocation"),
KGuiItem{i18n("Revoke Certification")},
KStandardGuiItem::cancel());
} else {
QStringList l;
Kleo::transform(certifications, std::back_inserter(l), [](const auto &c) {
return i18n("User ID '%1' certified with key %2", QString::fromUtf8(c.userId.id()), Formatting::formatForComboBox(c.certificationKey));
});
const auto message = i18np("You are about to revoke the following certification:", //
"You are about to revoke the following %1 certifications:",
certifications.size());
answer = KMessageBox::questionTwoActionsList(parent,
message,
l,
i18nc("@title:window", "Confirm Revocation"),
KGuiItem{i18n("Revoke Certifications")},
KStandardGuiItem::cancel());
}
return answer == KMessageBox::ButtonCode::PrimaryAction;
}
}
class RevokeCertificationCommand::Private : public Command::Private
{
friend class ::Kleo::Commands::RevokeCertificationCommand;
RevokeCertificationCommand *q_func() const
{
return static_cast(q);
}
public:
Private(InputType i, RevokeCertificationCommand *qq, KeyListController *c = nullptr);
void init();
private:
std::vector getCertificationsToRevoke();
void scheduleNextRevocation();
QGpgME::QuickJob *createJob();
void slotResult(const Error &err);
private:
InputType inputType = InputType::Key;
Key certificationTarget;
std::vector uids;
std::vector certificationsToRevoke;
std::vector completedRevocations;
QPointer job;
};
RevokeCertificationCommand::Private *RevokeCertificationCommand::d_func()
{
return static_cast(d.get());
}
const RevokeCertificationCommand::Private *RevokeCertificationCommand::d_func() const
{
return static_cast(d.get());
}
#define d d_func()
#define q q_func()
RevokeCertificationCommand::Private::Private(InputType i, RevokeCertificationCommand *qq, KeyListController *c)
: Command::Private{qq, c}
, inputType{i}
{
}
void RevokeCertificationCommand::Private::init()
{
const std::vector keys_ = keys();
if (keys_.size() != 1) {
qCWarning(KLEOPATRA_LOG) << q << "Expected exactly one key, but got" << keys_.size();
return;
}
if (keys_.front().protocol() != GpgME::OpenPGP) {
qCWarning(KLEOPATRA_LOG) << q << "Expected OpenPGP key, but got" << keys_.front().protocolAsString();
return;
}
certificationTarget = keys_.front();
}
std::vector RevokeCertificationCommand::Private::getCertificationsToRevoke()
{
if (inputType != InputType::Certifications) {
// ensure that the certifications of the key have been loaded
if (certificationTarget.userID(0).numSignatures() == 0) {
certificationTarget.update();
}
// build list of user IDs and revokable certifications
const auto userIDsToConsider = (inputType == InputType::Key) ? certificationTarget.userIDs() : uids;
for (const auto &userId : userIDsToConsider) {
Kleo::transform(getCertificationKeys(userId), std::back_inserter(certificationsToRevoke), [userId](const auto &k) {
return CertificationData{userId, k.key, k.signature};
});
}
}
Kleo::erase_if(certificationsToRevoke, [](const auto &c) {
return c.certificationKey.isNull();
});
return certificationsToRevoke;
}
void RevokeCertificationCommand::Private::scheduleNextRevocation()
{
if (!certificationsToRevoke.empty()) {
const auto nextCertification = certificationsToRevoke.back();
job = createJob();
if (!job) {
qCWarning(KLEOPATRA_LOG) << q << "Failed to create job";
finished();
return;
}
job->startRevokeSignature(certificationTarget, nextCertification.certificationKey, {nextCertification.userId});
} else {
if (std::any_of(completedRevocations.begin(), completedRevocations.end(), [](const auto &revocation) {
return revocation.signature.isExportable();
})) {
const auto message = xi18ncp("@info",
"The certification has been revoked successfully."
"Do you want to publish the revocation?",
"%1 certifications have been revoked successfully."
"Do you want to publish the revocations?",
completedRevocations.size());
const auto yesButton = KGuiItem{i18ncp("@action:button", "Publish Revocation", "Publish Revocations", completedRevocations.size()),
QIcon::fromTheme(QStringLiteral("view-certificate-export-server"))};
const auto answer = KMessageBox::questionTwoActions(parentWidgetOrView(),
message,
i18nc("@title:window", "Confirm Publication"),
yesButton,
KStandardGuiItem::cancel(),
{},
KMessageBox::Notify | KMessageBox::Dangerous);
if (answer == KMessageBox::ButtonCode::PrimaryAction) {
const auto cmd = new ExportOpenPGPCertsToServerCommand{certificationTarget};
cmd->start();
}
}
finished();
}
}
void RevokeCertificationCommand::Private::slotResult(const Error &err)
{
if (err.isCanceled()) {
canceled();
return;
}
if (err) {
const auto failedRevocation = certificationsToRevoke.back();
error(xi18nc("@info",
"The revocation of the certification of user ID%1made with key%2failed:"
"%3",
Formatting::prettyNameAndEMail(failedRevocation.userId),
Formatting::formatForComboBox(failedRevocation.certificationKey),
Formatting::errorAsString(err)));
finished();
return;
}
completedRevocations.push_back(certificationsToRevoke.back());
certificationsToRevoke.pop_back();
scheduleNextRevocation();
}
QGpgME::QuickJob *RevokeCertificationCommand::Private::createJob()
{
const auto j = QGpgME::openpgp()->quickJob();
if (j) {
connect(j, &QGpgME::Job::jobProgress, q, &Command::progress);
connect(j, &QGpgME::QuickJob::result, q, [this](const GpgME::Error &error) {
slotResult(error);
});
}
return j;
}
RevokeCertificationCommand::RevokeCertificationCommand(QAbstractItemView *v, KeyListController *c)
: Command{v, new Private{InputType::Key, this, c}}
{
d->init();
}
RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::Key &key)
: Command{key, new Private{InputType::Key, this}}
{
d->init();
}
RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::UserID &uid)
: Command{uid.parent(), new Private{InputType::UserIDs, this}}
{
std::vector(1, uid).swap(d->uids);
d->init();
}
RevokeCertificationCommand::RevokeCertificationCommand(const std::vector &uids)
: Command{uids.empty() ? Key{} : uids.front().parent(), new Private{InputType::UserIDs, this}}
{
d->uids = uids;
d->init();
}
RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::UserID::Signature &signature)
: Command{signature.parent().parent(), new Private{InputType::Certifications, this}}
{
if (!signature.isNull()) {
const Key certificationKey = KeyCache::instance()->findByKeyIDOrFingerprint(signature.signerKeyID());
d->certificationsToRevoke = {{signature.parent(), certificationKey, signature}};
}
d->init();
}
RevokeCertificationCommand::~RevokeCertificationCommand()
{
qCDebug(KLEOPATRA_LOG) << this << __func__;
}
// static
bool RevokeCertificationCommand::isSupported()
{
return engineInfo(GpgEngine).engineVersion() >= "2.2.24";
}
void RevokeCertificationCommand::doStart()
{
if (d->certificationTarget.isNull()) {
d->finished();
return;
}
- if (!Kleo::all_of(d->uids, userIDBelongsToKey(d->certificationTarget))) {
+ if (!std::ranges::all_of(d->uids, userIDBelongsToKey(d->certificationTarget))) {
qCWarning(KLEOPATRA_LOG) << this << "User ID <-> Key mismatch!";
d->finished();
return;
}
const auto certificationsToRevoke = d->getCertificationsToRevoke();
if (certificationsToRevoke.empty()) {
switch (d->inputType) {
case InputType::Key:
d->information(i18n("You cannot revoke any certifications of this key."));
break;
case InputType::UserIDs:
d->information(i18np("You cannot revoke any certifications of this user ID.", //
"You cannot revoke any certifications of these user IDs.",
d->uids.size()));
break;
case InputType::Certifications:
d->information(i18n("You cannot revoke this certification."));
break;
}
d->finished();
return;
}
if (!confirmRevocations(d->parentWidgetOrView(), certificationsToRevoke)) {
d->canceled();
return;
}
d->scheduleNextRevocation();
}
void RevokeCertificationCommand::doCancel()
{
qCDebug(KLEOPATRA_LOG) << this << __func__;
if (d->job) {
d->job->slotCancel();
}
}
#undef d
#undef q
#include "moc_revokecertificationcommand.cpp"
diff --git a/src/conf/groupsconfigwidget.cpp b/src/conf/groupsconfigwidget.cpp
index 348d14769..70523db0d 100644
--- a/src/conf/groupsconfigwidget.cpp
+++ b/src/conf/groupsconfigwidget.cpp
@@ -1,511 +1,511 @@
/*
conf/groupsconfigwidget.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2021 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "groupsconfigwidget.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "kleopatra_debug.h"
using namespace Kleo;
using namespace Kleo::Dialogs;
Q_DECLARE_METATYPE(KeyGroup)
namespace
{
class ListView : public QListView
{
Q_OBJECT
public:
using QListView::QListView;
protected:
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 2)
void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override
{
// workaround bug in QListView::currentChanged which sends an accessible focus event
// even if the list view doesn't have focus
if (hasFocus()) {
QListView::currentChanged(current, previous);
} else {
// skip the reimplementation of currentChanged in QListView
QAbstractItemView::currentChanged(current, previous);
}
}
#endif
void focusInEvent(QFocusEvent *event) override
{
QListView::focusInEvent(event);
// select current item if it isn't selected
if (currentIndex().isValid() && !selectionModel()->isSelected(currentIndex())) {
selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect);
}
}
};
class ProxyModel : public AbstractKeyListSortFilterProxyModel
{
Q_OBJECT
public:
ProxyModel(QObject *parent = nullptr)
: AbstractKeyListSortFilterProxyModel(parent)
{
}
~ProxyModel() override = default;
ProxyModel *clone() const override
{
// compiler-generated copy ctor is fine!
return new ProxyModel(*this);
}
int columnCount(const QModelIndex &parent = {}) const override
{
Q_UNUSED(parent)
// pretend that there is only one column to workaround a bug in
// QAccessibleTable which provides the accessibility interface for the
// list view
return 1;
}
QVariant data(const QModelIndex &idx, int role) const override
{
if (!idx.isValid()) {
return {};
}
return AbstractKeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role);
}
};
struct Selection {
KeyGroup current;
std::vector selected;
};
}
class GroupsConfigWidget::Private
{
friend class ::Kleo::GroupsConfigWidget;
GroupsConfigWidget *const q;
struct {
QLineEdit *groupsFilter = nullptr;
QListView *groupsList = nullptr;
QPushButton *newButton = nullptr;
QPushButton *editButton = nullptr;
QPushButton *deleteButton = nullptr;
QPushButton *certifyButton = nullptr;
QPushButton *exportButton = nullptr;
} ui;
AbstractKeyListModel *groupsModel = nullptr;
ProxyModel *groupsFilterModel = nullptr;
public:
Private(GroupsConfigWidget *qq)
: q(qq)
{
auto mainLayout = new QVBoxLayout(q);
mainLayout->setContentsMargins({});
auto groupsLayout = new QGridLayout;
groupsLayout->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
q->style()->pixelMetric(QStyle::PM_LayoutTopMargin),
q->style()->pixelMetric(QStyle::PM_LayoutRightMargin),
q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
groupsLayout->setColumnStretch(0, 1);
groupsLayout->setRowStretch(1, 1);
int row = -1;
row++;
{
auto hbox = new QHBoxLayout;
auto label = new QLabel{i18nc("@label", "Search:")};
label->setAccessibleName(i18nc("@label", "Search groups"));
label->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term."));
hbox->addWidget(label);
ui.groupsFilter = new QLineEdit(q);
ui.groupsFilter->setClearButtonEnabled(true);
ui.groupsFilter->setAccessibleName(i18nc("@label", "Search groups"));
ui.groupsFilter->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term."));
ui.groupsFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
ui.groupsFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
label->setBuddy(ui.groupsFilter);
hbox->addWidget(ui.groupsFilter, 1);
groupsLayout->addLayout(hbox, row, 0);
}
row++;
groupsModel = AbstractKeyListModel::createFlatKeyListModel(q);
groupsFilterModel = new ProxyModel(q);
groupsFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
groupsFilterModel->setFilterKeyColumn(KeyList::Summary);
groupsFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
groupsFilterModel->setSourceModel(groupsModel);
groupsFilterModel->sort(KeyList::Summary, Qt::AscendingOrder);
ui.groupsList = new ListView(q);
ui.groupsList->setAccessibleName(i18nc("groups of keys", "groups"));
ui.groupsList->setModel(groupsFilterModel);
ui.groupsList->setSelectionBehavior(QAbstractItemView::SelectRows);
ui.groupsList->setSelectionMode(QAbstractItemView::ExtendedSelection);
groupsLayout->addWidget(ui.groupsList, row, 0);
auto groupsButtonLayout = new QVBoxLayout;
ui.newButton = new QPushButton(i18nc("@action:button", "New"), q);
groupsButtonLayout->addWidget(ui.newButton);
ui.editButton = new QPushButton(i18nc("@action:button", "Edit"), q);
ui.editButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.editButton);
ui.deleteButton = new QPushButton(i18nc("@action:button", "Delete"), q);
ui.deleteButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.deleteButton);
ui.certifyButton = new QPushButton{i18nc("@action:button", "Certify"), q};
ui.certifyButton->setToolTip(i18nc("@info:tooltip", "Start the certification process for all certificates in the group."));
ui.certifyButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.certifyButton);
ui.exportButton = new QPushButton{i18nc("@action:button", "Export"), q};
ui.exportButton->setEnabled(false);
groupsButtonLayout->addWidget(ui.exportButton);
groupsButtonLayout->addStretch(1);
groupsLayout->addLayout(groupsButtonLayout, row, 1);
mainLayout->addLayout(groupsLayout, /*stretch=*/1);
connect(ui.groupsFilter, &QLineEdit::textChanged, q, [this](const auto &s) {
groupsFilterModel->setFilterRegularExpression(QRegularExpression::escape(s));
});
connect(ui.groupsList->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() {
selectionChanged();
});
connect(ui.groupsList, &QListView::doubleClicked, q, [this](const QModelIndex &index) {
editGroup(index);
});
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.certifyButton, &QPushButton::clicked, q, [this]() {
certifyGroup();
});
connect(ui.exportButton, &QPushButton::clicked, q, [this]() {
exportGroup();
});
}
private:
auto getGroupIndex(const KeyGroup &group)
{
QModelIndex index;
if (const KeyListModelInterface *const klmi = dynamic_cast(ui.groupsList->model())) {
index = klmi->index(group);
}
return index;
}
auto selectedRows()
{
return ui.groupsList->selectionModel()->selectedRows();
}
auto getGroup(const QModelIndex &index)
{
return index.isValid() ? ui.groupsList->model()->data(index, KeyList::GroupRole).value() : KeyGroup{};
}
auto getGroups(const QModelIndexList &indexes)
{
std::vector groups;
std::transform(std::begin(indexes), std::end(indexes), std::back_inserter(groups), [this](const auto &index) {
return getGroup(index);
});
return groups;
}
Selection saveSelection()
{
return {getGroup(ui.groupsList->selectionModel()->currentIndex()), getGroups(selectedRows())};
}
void restoreSelection(const Selection &selection)
{
auto selectionModel = ui.groupsList->selectionModel();
selectionModel->clearSelection();
for (const auto &group : selection.selected) {
selectionModel->select(getGroupIndex(group), QItemSelectionModel::Select | QItemSelectionModel::Rows);
}
auto currentIndex = getGroupIndex(selection.current);
if (currentIndex.isValid()) {
// keep current item if old current group is gone
selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate);
}
}
void selectionChanged()
{
const auto selectedGroups = getGroups(selectedRows());
const bool allSelectedGroupsAreEditable = std::all_of(std::begin(selectedGroups), std::end(selectedGroups), [](const auto &g) {
return !g.isNull() && !g.isImmutable();
});
ui.editButton->setEnabled(selectedGroups.size() == 1 && allSelectedGroupsAreEditable);
ui.deleteButton->setEnabled(!selectedGroups.empty() && allSelectedGroupsAreEditable);
ui.certifyButton->setEnabled(selectedGroups.size() == 1 //
&& !selectedGroups.front().keys().empty() //
&& allKeysHaveProtocol(selectedGroups.front().keys(), GpgME::OpenPGP));
ui.exportButton->setEnabled(selectedGroups.size() == 1);
}
KeyGroup showEditGroupDialog(KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget)
{
auto dialog = std::make_unique(q);
dialog->setWindowTitle(windowTitle);
dialog->setGroupName(group.name());
const KeyGroup::Keys &keys = group.keys();
dialog->setGroupKeys(std::vector(keys.cbegin(), keys.cend()));
dialog->setInitialFocus(focusWidget);
const int result = dialog->exec();
if (result == QDialog::Rejected) {
return KeyGroup();
}
group.setName(dialog->groupName());
group.setKeys(dialog->groupKeys());
return group;
}
void addGroup()
{
const KeyGroup::Id newId = KRandom::randomString(8);
KeyGroup group = KeyGroup(newId, i18nc("default name for new group of keys", "New Group"), {}, KeyGroup::ApplicationConfig);
group.setIsImmutable(false);
const KeyGroup newGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "New Group"), EditGroupDialog::GroupName);
if (newGroup.isNull()) {
return;
}
const QModelIndex newIndex = groupsModel->addGroup(newGroup);
if (!newIndex.isValid()) {
qCDebug(KLEOPATRA_LOG) << "Adding group to model failed";
return;
}
Q_EMIT q->changed();
}
void editGroup(const QModelIndex &index = {})
{
QModelIndex groupIndex;
if (index.isValid()) {
groupIndex = index;
} else {
const auto selection = selectedRows();
if (selection.size() != 1) {
qCDebug(KLEOPATRA_LOG) << (selection.empty() ? "selection is empty" : "more than one group is selected");
return;
}
groupIndex = selection.front();
}
const KeyGroup group = getGroup(groupIndex);
if (group.isNull()) {
qCDebug(KLEOPATRA_LOG) << "selected group is null";
return;
}
if (group.isImmutable()) {
qCDebug(KLEOPATRA_LOG) << "selected group is immutable";
return;
}
const KeyGroup updatedGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "Edit Group"), EditGroupDialog::KeysFilter);
if (updatedGroup.isNull()) {
return;
}
// look up index of updated group; the groupIndex used above may have become invalid
const auto updatedGroupIndex = getGroupIndex(updatedGroup);
if (updatedGroupIndex.isValid()) {
const bool success = ui.groupsList->model()->setData(updatedGroupIndex, QVariant::fromValue(updatedGroup));
if (!success) {
qCDebug(KLEOPATRA_LOG) << "Updating group in model failed";
return;
}
} else {
qCDebug(KLEOPATRA_LOG) << __func__ << "Failed to find index of group" << updatedGroup << "; maybe it was removed behind our back; re-add it";
const QModelIndex newIndex = groupsModel->addGroup(updatedGroup);
if (!newIndex.isValid()) {
qCDebug(KLEOPATRA_LOG) << "Re-adding group to model failed";
return;
}
}
Q_EMIT q->changed();
}
bool confirmDeletion(const std::vector &groups)
{
QString message;
QStringList groupSummaries;
if (groups.size() == 1) {
message = xi18nc("@info",
"Do you really want to delete this group?"
"%1"
"Once deleted, it cannot be restored.")
.arg(Formatting::summaryLine(groups.front()));
} else {
message = xi18ncp("@info",
"Do you really want to delete this %1 group?"
"Once deleted, it cannot be restored.",
"Do you really want to delete these %1 groups?"
"Once deleted, they cannot be restored.",
groups.size());
Kleo::transform(groups, std::back_inserter(groupSummaries), [](const auto &g) {
return Formatting::summaryLine(g);
});
}
const auto answer = KMessageBox::questionTwoActionsList(q,
message,
groupSummaries,
i18ncp("@title:window", "Delete Group", "Delete Groups", groups.size()),
KStandardGuiItem::del(),
KStandardGuiItem::cancel(),
{},
KMessageBox::Notify | KMessageBox::Dangerous);
return answer == KMessageBox::PrimaryAction;
}
void deleteGroup()
{
const auto selectedGroups = getGroups(selectedRows());
if (selectedGroups.empty()) {
qCDebug(KLEOPATRA_LOG) << "selection is empty";
return;
}
if (!confirmDeletion(selectedGroups)) {
return;
}
for (const auto &group : selectedGroups) {
const bool success = groupsModel->removeGroup(group);
if (!success) {
qCDebug(KLEOPATRA_LOG) << "Removing group from model failed:" << group;
}
}
Q_EMIT q->changed();
}
void certifyGroup()
{
const auto selectedGroups = getGroups(selectedRows());
if (selectedGroups.size() != 1) {
qCDebug(KLEOPATRA_LOG) << __func__ << (selectedGroups.empty() ? "selection is empty" : "more than one group is selected");
return;
}
auto cmd = new CertifyGroupCommand{selectedGroups.front()};
cmd->setParentWidget(q->window());
cmd->start();
}
void exportGroup()
{
const auto selectedGroups = getGroups(selectedRows());
if (selectedGroups.empty()) {
qCDebug(KLEOPATRA_LOG) << "selection is empty";
return;
}
- if (Kleo::any_of(selectedGroups[0].keys(), [](const auto &key) {
+ if (std::ranges::any_of(selectedGroups[0].keys(), [](const auto &key) {
return !Kleo::keyHasEncrypt(key);
})) {
KMessageBox::information(
q->parentWidget(),
i18nc("@info", "The group contains certificates that cannot be used for encryption. This may lead to unexpected results."));
}
auto cmd = new ExportGroupsCommand(selectedGroups);
cmd->start();
}
};
GroupsConfigWidget::GroupsConfigWidget(QWidget *parent)
: QWidget(parent)
, d(new Private(this))
{
}
GroupsConfigWidget::~GroupsConfigWidget() = default;
void GroupsConfigWidget::setGroups(const std::vector &groups)
{
const auto selection = d->saveSelection();
d->groupsModel->setGroups(groups);
d->restoreSelection(selection);
}
std::vector GroupsConfigWidget::groups() const
{
std::vector result;
result.reserve(d->groupsModel->rowCount());
for (int row = 0; row < d->groupsModel->rowCount(); ++row) {
const QModelIndex index = d->groupsModel->index(row, 0);
result.push_back(d->groupsModel->group(index));
}
return result;
}
#include "groupsconfigwidget.moc"
#include "moc_groupsconfigwidget.cpp"
diff --git a/src/crypto/gui/signencryptwidget.cpp b/src/crypto/gui/signencryptwidget.cpp
index c0cad03b6..892eaec5b 100644
--- a/src/crypto/gui/signencryptwidget.cpp
+++ b/src/crypto/gui/signencryptwidget.cpp
@@ -1,895 +1,895 @@
/* crypto/gui/signencryptwidget.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik
SPDX-FileContributor: Intevation GmbH
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "signencryptwidget.h"
#include "kleopatra_debug.h"
#include "certificatelineedit.h"
#include "fileoperationspreferences.h"
#include "kleopatraapplication.h"
#include "settings.h"
#include "unknownrecipientwidget.h"
#include "dialogs/certificateselectiondialog.h"
#include "utils/gui-helper.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace Kleo;
using namespace Kleo::Dialogs;
using namespace GpgME;
namespace
{
class SignCertificateFilter : public DefaultKeyFilter
{
public:
SignCertificateFilter(GpgME::Protocol proto)
: DefaultKeyFilter()
{
setIsBad(DefaultKeyFilter::NotSet);
setHasSecret(DefaultKeyFilter::Set);
setCanSign(DefaultKeyFilter::Set);
setValidIfSMIME(DefaultKeyFilter::Set);
if (proto == GpgME::OpenPGP) {
setIsOpenPGP(DefaultKeyFilter::Set);
} else if (proto == GpgME::CMS) {
setIsOpenPGP(DefaultKeyFilter::NotSet);
}
}
};
class EncryptCertificateFilter : public DefaultKeyFilter
{
public:
EncryptCertificateFilter(GpgME::Protocol proto)
: DefaultKeyFilter()
{
setIsBad(DefaultKeyFilter::NotSet);
setCanEncrypt(DefaultKeyFilter::Set);
setValidIfSMIME(DefaultKeyFilter::Set);
if (proto == GpgME::OpenPGP) {
setIsOpenPGP(DefaultKeyFilter::Set);
} else if (proto == GpgME::CMS) {
setIsOpenPGP(DefaultKeyFilter::NotSet);
}
}
};
class EncryptSelfCertificateFilter : public EncryptCertificateFilter
{
public:
EncryptSelfCertificateFilter(GpgME::Protocol proto)
: EncryptCertificateFilter(proto)
{
setRevoked(DefaultKeyFilter::NotSet);
setExpired(DefaultKeyFilter::NotSet);
setCanEncrypt(DefaultKeyFilter::Set);
setHasSecret(DefaultKeyFilter::Set);
setValidIfSMIME(DefaultKeyFilter::Set);
}
};
}
class SignEncryptWidget::Private
{
SignEncryptWidget *const q;
public:
struct RecipientWidgets {
CertificateLineEdit *edit;
KMessageWidget *expiryMessage;
};
explicit Private(SignEncryptWidget *qq, bool sigEncExclusive)
: q{qq}
, mModel{AbstractKeyListModel::createFlatKeyListModel(qq)}
, mIsExclusive{sigEncExclusive}
{
}
CertificateLineEdit *addRecipientWidget();
/* Inserts a new recipient widget after widget @p after or at the end
* if @p after is null. */
CertificateLineEdit *insertRecipientWidget(CertificateLineEdit *after);
void recpRemovalRequested(const RecipientWidgets &recipient);
void onProtocolChanged();
void updateCheckBoxes();
ExpiryChecker *expiryChecker();
void updateExpiryMessages(KMessageWidget *w, const GpgME::Key &key, ExpiryChecker::CheckFlags flags);
void updateAllExpiryMessages();
public:
UserIDSelectionCombo *mSigSelect = nullptr;
KMessageWidget *mSignKeyExpiryMessage = nullptr;
UserIDSelectionCombo *mSelfSelect = nullptr;
KMessageWidget *mEncryptToSelfKeyExpiryMessage = nullptr;
std::vector mRecpWidgets;
QList mUnknownWidgets;
QList mAddedKeys;
QList mAddedGroups;
QVBoxLayout *mRecpLayout = nullptr;
Operations mOp;
AbstractKeyListModel *mModel = nullptr;
QCheckBox *mSymmetric = nullptr;
QCheckBox *mSigChk = nullptr;
QCheckBox *mEncOtherChk = nullptr;
QCheckBox *mEncSelfChk = nullptr;
GpgME::Protocol mCurrentProto = GpgME::UnknownProtocol;
const bool mIsExclusive;
std::unique_ptr mExpiryChecker;
};
SignEncryptWidget::SignEncryptWidget(QWidget *parent, bool sigEncExclusive)
: QWidget{parent}
, d{new Private{this, sigEncExclusive}}
{
auto lay = new QVBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
d->mModel->useKeyCache(true, KeyList::IncludeGroups);
const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty();
const bool havePublicKeys = !KeyCache::instance()->keys().empty();
const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly();
/* The signature selection */
{
auto sigGrp = new QGroupBox{i18nc("@title:group", "Prove authenticity (sign)"), this};
d->mSigChk = new QCheckBox{i18n("Sign as:"), this};
d->mSigChk->setEnabled(haveSecretKeys);
d->mSigChk->setChecked(haveSecretKeys);
d->mSigSelect = new UserIDSelectionCombo{KeyUsage::Sign, this};
d->mSigSelect->setEnabled(d->mSigChk->isChecked());
d->mSignKeyExpiryMessage = new KMessageWidget{this};
d->mSignKeyExpiryMessage->setVisible(false);
auto groupLayout = new QGridLayout{sigGrp};
groupLayout->setColumnStretch(1, 1);
groupLayout->addWidget(d->mSigChk, 0, 0);
groupLayout->addWidget(d->mSigSelect, 0, 1);
groupLayout->addWidget(d->mSignKeyExpiryMessage, 1, 1);
lay->addWidget(sigGrp);
connect(d->mSigChk, &QCheckBox::toggled, this, [this](bool checked) {
d->mSigSelect->setEnabled(checked);
updateOp();
d->updateExpiryMessages(d->mSignKeyExpiryMessage, signKey(), ExpiryChecker::OwnSigningKey);
});
connect(d->mSigSelect, &UserIDSelectionCombo::currentKeyChanged, this, [this]() {
updateOp();
d->updateExpiryMessages(d->mSignKeyExpiryMessage, signKey(), ExpiryChecker::OwnSigningKey);
});
}
// Recipient selection
{
auto encBox = new QGroupBox{i18nc("@title:group", "Encrypt"), this};
auto encBoxLay = new QVBoxLayout{encBox};
auto recipientGrid = new QGridLayout;
int row = 0;
// Own key
d->mEncSelfChk = new QCheckBox{i18n("Encrypt for me:"), this};
d->mEncSelfChk->setEnabled(haveSecretKeys && !symmetricOnly);
d->mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly);
d->mSelfSelect = new UserIDSelectionCombo{KeyUsage::Encrypt, this};
d->mSelfSelect->setEnabled(d->mEncSelfChk->isChecked());
d->mEncryptToSelfKeyExpiryMessage = new KMessageWidget{this};
d->mEncryptToSelfKeyExpiryMessage->setVisible(false);
recipientGrid->addWidget(d->mEncSelfChk, row, 0);
recipientGrid->addWidget(d->mSelfSelect, row, 1);
row++;
recipientGrid->addWidget(d->mEncryptToSelfKeyExpiryMessage, row, 1);
// Checkbox for other keys
row++;
d->mEncOtherChk = new QCheckBox{i18n("Encrypt for others:"), this};
d->mEncOtherChk->setEnabled(havePublicKeys && !symmetricOnly);
d->mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly);
recipientGrid->addWidget(d->mEncOtherChk, row, 0, Qt::AlignTop);
connect(d->mEncOtherChk, &QCheckBox::toggled, this, [this](bool checked) {
for (const auto &recipient : std::as_const(d->mRecpWidgets)) {
recipient.edit->setEnabled(checked);
d->updateExpiryMessages(recipient.expiryMessage, checked ? recipient.edit->key() : Key{}, ExpiryChecker::EncryptionKey);
}
updateOp();
});
d->mRecpLayout = new QVBoxLayout;
recipientGrid->addLayout(d->mRecpLayout, row, 1);
recipientGrid->setRowStretch(row + 1, 1);
// Scroll area for other keys
auto recipientWidget = new QWidget;
auto recipientScroll = new QScrollArea;
recipientWidget->setLayout(recipientGrid);
recipientScroll->setWidget(recipientWidget);
recipientScroll->setWidgetResizable(true);
recipientScroll->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
recipientScroll->setFrameStyle(QFrame::NoFrame);
recipientScroll->setFocusPolicy(Qt::NoFocus);
recipientGrid->setContentsMargins(0, 0, 0, 0);
encBoxLay->addWidget(recipientScroll, 1);
auto bar = recipientScroll->verticalScrollBar();
connect(bar, &QScrollBar::rangeChanged, this, [bar](int, int max) {
bar->setValue(max);
});
d->addRecipientWidget();
// Checkbox for password
d->mSymmetric = new QCheckBox(i18n("Encrypt with password. Anyone you share the password with can read the data."));
d->mSymmetric->setToolTip(i18nc("Tooltip information for symmetric encryption",
"Additionally to the keys of the recipients you can encrypt your data with a password. "
"Anyone who has the password can read the data without any secret key. "
"Using a password is less secure then public key cryptography. Even if you pick a very strong password."));
d->mSymmetric->setChecked(symmetricOnly || !havePublicKeys);
encBoxLay->addWidget(d->mSymmetric);
// Connect it
connect(d->mEncSelfChk, &QCheckBox::toggled, this, [this](bool checked) {
d->mSelfSelect->setEnabled(checked);
updateOp();
d->updateExpiryMessages(d->mEncryptToSelfKeyExpiryMessage, selfKey(), ExpiryChecker::OwnEncryptionKey);
});
connect(d->mSelfSelect, &UserIDSelectionCombo::currentKeyChanged, this, [this]() {
updateOp();
d->updateExpiryMessages(d->mEncryptToSelfKeyExpiryMessage, selfKey(), ExpiryChecker::OwnEncryptionKey);
});
connect(d->mSymmetric, &QCheckBox::toggled, this, &SignEncryptWidget::updateOp);
if (d->mIsExclusive) {
connect(d->mEncOtherChk, &QCheckBox::toggled, this, [this](bool value) {
if (d->mCurrentProto != GpgME::CMS) {
return;
}
if (value) {
d->mSigChk->setChecked(false);
}
});
connect(d->mEncSelfChk, &QCheckBox::toggled, this, [this](bool value) {
if (d->mCurrentProto != GpgME::CMS) {
return;
}
if (value) {
d->mSigChk->setChecked(false);
}
});
connect(d->mSigChk, &QCheckBox::toggled, this, [this](bool value) {
if (d->mCurrentProto != GpgME::CMS) {
return;
}
if (value) {
d->mEncSelfChk->setChecked(false);
d->mEncOtherChk->setChecked(false);
}
});
}
// Ensure that the d->mSigChk is aligned together with the encryption check boxes.
d->mSigChk->setMinimumWidth(qMax(d->mEncOtherChk->width(), d->mEncSelfChk->width()));
lay->addWidget(encBox);
}
connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, [this]() {
d->updateCheckBoxes();
d->updateAllExpiryMessages();
});
connect(KleopatraApplication::instance(), &KleopatraApplication::configurationChanged, this, [this]() {
d->updateCheckBoxes();
d->mExpiryChecker.reset();
d->updateAllExpiryMessages();
});
loadKeys();
d->onProtocolChanged();
updateOp();
}
SignEncryptWidget::~SignEncryptWidget() = default;
void SignEncryptWidget::setSignAsText(const QString &text)
{
d->mSigChk->setText(text);
}
void SignEncryptWidget::setEncryptForMeText(const QString &text)
{
d->mEncSelfChk->setText(text);
}
void SignEncryptWidget::setEncryptForOthersText(const QString &text)
{
d->mEncOtherChk->setText(text);
}
void SignEncryptWidget::setEncryptWithPasswordText(const QString &text)
{
d->mSymmetric->setText(text);
}
CertificateLineEdit *SignEncryptWidget::Private::addRecipientWidget()
{
return insertRecipientWidget(nullptr);
}
CertificateLineEdit *SignEncryptWidget::Private::insertRecipientWidget(CertificateLineEdit *after)
{
Q_ASSERT(!after || mRecpLayout->indexOf(after) != -1);
const auto index = after ? mRecpLayout->indexOf(after) + 2 : mRecpLayout->count();
const RecipientWidgets recipient{new CertificateLineEdit{mModel, KeyUsage::Encrypt, new EncryptCertificateFilter{mCurrentProto}, q}, new KMessageWidget{q}};
recipient.edit->setAccessibleNameOfLineEdit(i18nc("text for screen readers", "recipient key"));
recipient.edit->setEnabled(mEncOtherChk->isChecked());
recipient.expiryMessage->setVisible(false);
if (static_cast(index / 2) < mRecpWidgets.size()) {
mRecpWidgets.insert(mRecpWidgets.begin() + index / 2, recipient);
} else {
mRecpWidgets.push_back(recipient);
}
if (mRecpLayout->count() > 0) {
auto prevWidget = after ? after : mRecpLayout->itemAt(mRecpLayout->count() - 1)->widget();
Kleo::forceSetTabOrder(prevWidget, recipient.edit);
Kleo::forceSetTabOrder(recipient.edit, recipient.expiryMessage);
}
mRecpLayout->insertWidget(index, recipient.edit);
mRecpLayout->insertWidget(index + 1, recipient.expiryMessage);
connect(recipient.edit, &CertificateLineEdit::keyChanged, q, &SignEncryptWidget::recipientsChanged);
connect(recipient.edit, &CertificateLineEdit::editingStarted, q, &SignEncryptWidget::recipientsChanged);
connect(recipient.edit, &CertificateLineEdit::cleared, q, &SignEncryptWidget::recipientsChanged);
connect(recipient.edit, &CertificateLineEdit::certificateSelectionRequested, q, [this, recipient]() {
q->certificateSelectionRequested(recipient.edit);
});
return recipient.edit;
}
void SignEncryptWidget::addRecipient(const Key &key)
{
CertificateLineEdit *certSel = d->addRecipientWidget();
if (!key.isNull()) {
certSel->setKey(key);
d->mAddedKeys << key;
}
}
void SignEncryptWidget::addRecipient(const KeyGroup &group)
{
CertificateLineEdit *certSel = d->addRecipientWidget();
if (!group.isNull()) {
certSel->setGroup(group);
d->mAddedGroups << group;
}
}
void SignEncryptWidget::certificateSelectionRequested(CertificateLineEdit *certificateLineEdit)
{
CertificateSelectionDialog dlg{this};
dlg.setOptions(CertificateSelectionDialog::Options( //
CertificateSelectionDialog::MultiSelection | //
CertificateSelectionDialog::EncryptOnly | //
CertificateSelectionDialog::optionsFromProtocol(d->mCurrentProto) | //
CertificateSelectionDialog::IncludeGroups));
if (!certificateLineEdit->key().isNull()) {
const auto key = certificateLineEdit->key();
const auto name = QString::fromUtf8(key.userID(0).name());
const auto email = QString::fromUtf8(key.userID(0).email());
dlg.setStringFilter(!name.isEmpty() ? name : email);
} else if (!certificateLineEdit->group().isNull()) {
dlg.setStringFilter(certificateLineEdit->group().name());
} else if (!certificateLineEdit->userID().isNull()) {
const auto userID = certificateLineEdit->userID();
const auto name = QString::fromUtf8(userID.name());
const auto email = QString::fromUtf8(userID.email());
dlg.setStringFilter(!name.isEmpty() ? name : email);
} else {
dlg.setStringFilter(certificateLineEdit->text());
}
if (dlg.exec()) {
const std::vector userIds = dlg.selectedUserIDs();
const std::vector groups = dlg.selectedGroups();
if (userIds.size() == 0 && groups.size() == 0) {
return;
}
CertificateLineEdit *certWidget = nullptr;
for (const auto &userId : userIds) {
if (!certWidget) {
certWidget = certificateLineEdit;
} else {
certWidget = d->insertRecipientWidget(certWidget);
}
certWidget->setUserID(userId);
}
for (const KeyGroup &group : groups) {
if (!certWidget) {
certWidget = certificateLineEdit;
} else {
certWidget = d->insertRecipientWidget(certWidget);
}
certWidget->setGroup(group);
}
}
recipientsChanged();
}
void SignEncryptWidget::clearAddedRecipients()
{
for (auto w : std::as_const(d->mUnknownWidgets)) {
d->mRecpLayout->removeWidget(w);
delete w;
}
for (auto &key : std::as_const(d->mAddedKeys)) {
removeRecipient(key);
}
for (auto &group : std::as_const(d->mAddedGroups)) {
removeRecipient(group);
}
}
void SignEncryptWidget::addUnknownRecipient(const char *keyID)
{
auto unknownWidget = new UnknownRecipientWidget(keyID);
d->mUnknownWidgets << unknownWidget;
if (d->mRecpLayout->count() > 0) {
auto lastWidget = d->mRecpLayout->itemAt(d->mRecpLayout->count() - 1)->widget();
setTabOrder(lastWidget, unknownWidget);
}
d->mRecpLayout->addWidget(unknownWidget);
connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, [this]() {
// Check if any unknown recipient can now be found.
for (auto w : d->mUnknownWidgets) {
auto key = KeyCache::instance()->findByKeyIDOrFingerprint(w->keyID().toLatin1().constData());
if (key.isNull()) {
std::vector subids;
subids.push_back(std::string(w->keyID().toLatin1().constData()));
for (const auto &subkey : KeyCache::instance()->findSubkeysByKeyID(subids)) {
key = subkey.parent();
}
}
if (key.isNull()) {
continue;
}
// Key is now available replace by line edit.
qCDebug(KLEOPATRA_LOG) << "Removing widget for keyid: " << w->keyID();
d->mRecpLayout->removeWidget(w);
d->mUnknownWidgets.removeAll(w);
delete w;
addRecipient(key);
}
});
}
void SignEncryptWidget::recipientsChanged()
{
const bool hasEmptyRecpWidget = std::any_of(std::cbegin(d->mRecpWidgets), std::cend(d->mRecpWidgets), [](auto w) {
return w.edit->isEmpty();
});
if (!hasEmptyRecpWidget) {
d->addRecipientWidget();
}
updateOp();
for (const auto &recipient : std::as_const(d->mRecpWidgets)) {
if (!recipient.edit->isEditingInProgress() || recipient.edit->isEmpty()) {
d->updateExpiryMessages(recipient.expiryMessage, d->mEncOtherChk->isChecked() ? recipient.edit->key() : Key{}, ExpiryChecker::EncryptionKey);
}
}
}
Key SignEncryptWidget::signKey() const
{
if (d->mSigSelect->isEnabled()) {
return d->mSigSelect->currentKey();
}
return Key();
}
Key SignEncryptWidget::selfKey() const
{
if (d->mSelfSelect->isEnabled()) {
return d->mSelfSelect->currentKey();
}
return Key();
}
std::vector SignEncryptWidget::recipients() const
{
std::vector ret;
for (const auto &recipient : std::as_const(d->mRecpWidgets)) {
const auto *const w = recipient.edit;
if (!w->isEnabled()) {
// If one is disabled, all are disabled.
break;
}
const Key k = w->key();
const KeyGroup g = w->group();
const UserID u = w->userID();
if (!k.isNull()) {
ret.push_back(k);
} else if (!g.isNull()) {
const auto keys = g.keys();
std::copy(keys.begin(), keys.end(), std::back_inserter(ret));
} else if (!u.isNull()) {
ret.push_back(u.parent());
}
}
const Key k = selfKey();
if (!k.isNull()) {
ret.push_back(k);
}
return ret;
}
bool SignEncryptWidget::isDeVsAndValid() const
{
if (!signKey().isNull() && !DeVSCompliance::keyIsCompliant(signKey())) {
return false;
}
if (!selfKey().isNull() && !DeVSCompliance::keyIsCompliant(selfKey())) {
return false;
}
for (const auto &key : recipients()) {
if (!DeVSCompliance::keyIsCompliant(key)) {
return false;
}
}
return true;
}
static QString expiryMessage(const ExpiryChecker::Result &result)
{
if (result.expiration.certificate.isNull()) {
return {};
}
switch (result.expiration.status) {
case ExpiryChecker::Expired:
return i18nc("@info", "This certificate is expired.");
case ExpiryChecker::ExpiresSoon: {
if (result.expiration.duration.count() == 0) {
return i18nc("@info", "This certificate expires today.");
} else {
return i18ncp("@info", "This certificate expires tomorrow.", "This certificate expires in %1 days.", result.expiration.duration.count());
}
}
case ExpiryChecker::NoSuitableSubkey:
if (result.checkFlags & ExpiryChecker::EncryptionKey) {
return i18nc("@info", "This certificate cannot be used for encryption.");
} else {
return i18nc("@info", "This certificate cannot be used for signing.");
}
case ExpiryChecker::InvalidKey:
case ExpiryChecker::InvalidCheckFlags:
break; // wrong usage of ExpiryChecker; can be ignored
case ExpiryChecker::NotNearExpiry:;
}
return {};
}
void SignEncryptWidget::updateOp()
{
const Key sigKey = signKey();
const std::vector recp = recipients();
Operations op = NoOperation;
if (!sigKey.isNull()) {
op |= Sign;
}
if (!recp.empty() || encryptSymmetric()) {
op |= Encrypt;
}
d->mOp = op;
Q_EMIT operationChanged(d->mOp);
Q_EMIT keysChanged();
}
SignEncryptWidget::Operations SignEncryptWidget::currentOp() const
{
return d->mOp;
}
namespace
{
bool recipientWidgetHasFocus(QWidget *w)
{
// check if w (or its focus proxy) or a child widget of w has focus
return w->hasFocus() || w->isAncestorOf(qApp->focusWidget());
}
}
void SignEncryptWidget::Private::recpRemovalRequested(const RecipientWidgets &recipient)
{
if (!recipient.edit) {
return;
}
const int emptyEdits = std::count_if(std::cbegin(mRecpWidgets), std::cend(mRecpWidgets), [](const auto &r) {
return r.edit->isEmpty();
});
if (emptyEdits > 1) {
if (recipientWidgetHasFocus(recipient.edit) || recipientWidgetHasFocus(recipient.expiryMessage)) {
const int index = mRecpLayout->indexOf(recipient.edit);
const auto focusWidget = (index < mRecpLayout->count() - 2) ? //
mRecpLayout->itemAt(index + 2)->widget()
: mRecpLayout->itemAt(mRecpLayout->count() - 3)->widget();
focusWidget->setFocus();
}
mRecpLayout->removeWidget(recipient.expiryMessage);
mRecpLayout->removeWidget(recipient.edit);
const auto it = std::find_if(std::begin(mRecpWidgets), std::end(mRecpWidgets), [recipient](const auto &r) {
return r.edit == recipient.edit;
});
mRecpWidgets.erase(it);
recipient.expiryMessage->deleteLater();
recipient.edit->deleteLater();
}
}
void SignEncryptWidget::removeRecipient(const GpgME::Key &key)
{
for (const auto &recipient : std::as_const(d->mRecpWidgets)) {
const auto editKey = recipient.edit->key();
if (key.isNull() && editKey.isNull()) {
d->recpRemovalRequested(recipient);
return;
}
if (editKey.primaryFingerprint() && key.primaryFingerprint() && !strcmp(editKey.primaryFingerprint(), key.primaryFingerprint())) {
d->recpRemovalRequested(recipient);
return;
}
}
}
void SignEncryptWidget::removeRecipient(const KeyGroup &group)
{
for (const auto &recipient : std::as_const(d->mRecpWidgets)) {
const auto editGroup = recipient.edit->group();
if (group.isNull() && editGroup.isNull()) {
d->recpRemovalRequested(recipient);
return;
}
if (editGroup.name() == group.name()) {
d->recpRemovalRequested(recipient);
return;
}
}
}
bool SignEncryptWidget::encryptSymmetric() const
{
return d->mSymmetric->isChecked();
}
void SignEncryptWidget::loadKeys()
{
KConfigGroup keys(KSharedConfig::openConfig(), QStringLiteral("SignEncryptKeys"));
auto cache = KeyCache::instance();
d->mSigSelect->setDefaultKey(keys.readEntry("SigningKey", QString()));
d->mSelfSelect->setDefaultKey(keys.readEntry("EncryptKey", QString()));
}
void SignEncryptWidget::saveOwnKeys() const
{
KConfigGroup keys(KSharedConfig::openConfig(), QStringLiteral("SignEncryptKeys"));
auto sigKey = d->mSigSelect->currentKey();
auto encKey = d->mSelfSelect->currentKey();
if (!sigKey.isNull()) {
keys.writeEntry("SigningKey", sigKey.primaryFingerprint());
}
if (!encKey.isNull()) {
keys.writeEntry("EncryptKey", encKey.primaryFingerprint());
}
}
void SignEncryptWidget::setSigningChecked(bool value)
{
d->mSigChk->setChecked(value && !KeyCache::instance()->secretKeys().empty());
}
void SignEncryptWidget::setEncryptionChecked(bool checked)
{
if (checked) {
const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty();
const bool havePublicKeys = !KeyCache::instance()->keys().empty();
const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly();
d->mEncSelfChk->setChecked(haveSecretKeys && !symmetricOnly);
d->mEncOtherChk->setChecked(havePublicKeys && !symmetricOnly);
d->mSymmetric->setChecked(symmetricOnly || !havePublicKeys);
} else {
d->mEncSelfChk->setChecked(false);
d->mEncOtherChk->setChecked(false);
d->mSymmetric->setChecked(false);
}
}
void SignEncryptWidget::setProtocol(GpgME::Protocol proto)
{
if (d->mCurrentProto == proto) {
return;
}
d->mCurrentProto = proto;
d->onProtocolChanged();
}
void Kleo::SignEncryptWidget::Private::onProtocolChanged()
{
mSigSelect->setKeyFilter(std::shared_ptr(new SignCertificateFilter(mCurrentProto)));
mSelfSelect->setKeyFilter(std::shared_ptr(new EncryptSelfCertificateFilter(mCurrentProto)));
const auto encFilter = std::shared_ptr(new EncryptCertificateFilter(mCurrentProto));
for (const auto &recipient : std::as_const(mRecpWidgets)) {
recipient.edit->setKeyFilter(encFilter);
}
if (mIsExclusive) {
mSymmetric->setDisabled(mCurrentProto == GpgME::CMS);
if (mSymmetric->isChecked() && mCurrentProto == GpgME::CMS) {
mSymmetric->setChecked(false);
}
if (mSigChk->isChecked() && mCurrentProto == GpgME::CMS && (mEncSelfChk->isChecked() || mEncOtherChk->isChecked())) {
mSigChk->setChecked(false);
}
}
}
static bool recipientIsOkay(const CertificateLineEdit *edit)
{
if (!edit->isEnabled() || edit->isEmpty()) {
return true;
}
if (!edit->hasAcceptableInput()) {
return false;
}
if (const auto userID = edit->userID(); !userID.isNull()) {
return Kleo::canBeUsedForEncryption(userID.parent()) && !userID.isBad();
}
if (const auto key = edit->key(); !key.isNull()) {
return Kleo::canBeUsedForEncryption(key);
}
if (const auto group = edit->group(); !group.isNull()) {
- return Kleo::all_of(group.keys(), Kleo::canBeUsedForEncryption);
+ return std::ranges::all_of(group.keys(), Kleo::canBeUsedForEncryption);
}
// we should never reach this point
return false;
}
bool SignEncryptWidget::isComplete() const
{
if (currentOp() == NoOperation) {
return false;
}
if ((currentOp() & SignEncryptWidget::Sign) && !Kleo::canBeUsedForSigning(signKey())) {
return false;
}
if (currentOp() & SignEncryptWidget::Encrypt) {
if (!selfKey().isNull() && !Kleo::canBeUsedForEncryption(selfKey())) {
return false;
}
- const bool allOtherRecipientsAreOkay = Kleo::all_of(d->mRecpWidgets, [](const auto &r) {
+ const bool allOtherRecipientsAreOkay = std::ranges::all_of(d->mRecpWidgets, [](const auto &r) {
return recipientIsOkay(r.edit);
});
if (!allOtherRecipientsAreOkay) {
return false;
}
}
return true;
}
bool SignEncryptWidget::validate()
{
CertificateLineEdit *firstUnresolvedRecipient = nullptr;
QStringList unresolvedRecipients;
for (const auto &recipient : std::as_const(d->mRecpWidgets)) {
if (recipient.edit->isEnabled() && !recipient.edit->hasAcceptableInput()) {
if (!firstUnresolvedRecipient) {
firstUnresolvedRecipient = recipient.edit;
}
unresolvedRecipients.push_back(recipient.edit->text().toHtmlEscaped());
}
}
if (!unresolvedRecipients.isEmpty()) {
KMessageBox::errorList(this,
i18n("Could not find a key for the following recipients:"),
unresolvedRecipients,
i18nc("@title:window", "Failed to find some keys"));
}
if (firstUnresolvedRecipient) {
firstUnresolvedRecipient->setFocus();
}
return unresolvedRecipients.isEmpty();
}
void SignEncryptWidget::Private::updateCheckBoxes()
{
const bool haveSecretKeys = !KeyCache::instance()->secretKeys().empty();
const bool havePublicKeys = !KeyCache::instance()->keys().empty();
const bool symmetricOnly = FileOperationsPreferences().symmetricEncryptionOnly();
mSigChk->setEnabled(haveSecretKeys);
mEncSelfChk->setEnabled(haveSecretKeys && !symmetricOnly);
mEncOtherChk->setEnabled(havePublicKeys && !symmetricOnly);
if (symmetricOnly) {
mEncSelfChk->setChecked(false);
mEncOtherChk->setChecked(false);
mSymmetric->setChecked(true);
}
}
ExpiryChecker *Kleo::SignEncryptWidget::Private::expiryChecker()
{
if (!mExpiryChecker) {
mExpiryChecker.reset(new ExpiryChecker{ExpiryCheckerConfig{}.settings()});
}
return mExpiryChecker.get();
}
void SignEncryptWidget::Private::updateExpiryMessages(KMessageWidget *messageWidget, const GpgME::Key &key, ExpiryChecker::CheckFlags flags)
{
messageWidget->setCloseButtonVisible(false);
if (!Settings{}.showExpiryNotifications() || key.isNull()) {
messageWidget->setVisible(false);
} else {
const auto result = expiryChecker()->checkKey(key, flags);
const auto message = expiryMessage(result);
messageWidget->setText(message);
messageWidget->setVisible(!message.isEmpty());
}
}
void SignEncryptWidget::Private::updateAllExpiryMessages()
{
updateExpiryMessages(mSignKeyExpiryMessage, q->signKey(), ExpiryChecker::OwnSigningKey);
updateExpiryMessages(mEncryptToSelfKeyExpiryMessage, q->selfKey(), ExpiryChecker::OwnEncryptionKey);
for (const auto &recipient : std::as_const(mRecpWidgets)) {
if (recipient.edit->isEnabled()) {
updateExpiryMessages(recipient.expiryMessage, recipient.edit->key(), ExpiryChecker::EncryptionKey);
}
}
}
#include "moc_signencryptwidget.cpp"
diff --git a/src/dialogs/certifycertificatedialog.cpp b/src/dialogs/certifycertificatedialog.cpp
index 7bde705c6..adb0acbc6 100644
--- a/src/dialogs/certifycertificatedialog.cpp
+++ b/src/dialogs/certifycertificatedialog.cpp
@@ -1,156 +1,156 @@
/* -*- mode: c++; c-basic-offset:4 -*-
dialogs/signcertificatedialog.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
SPDX-FileCopyrightText: 2019 g10code GmbH
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "kleopatra_debug.h"
#include "certifycertificatedialog.h"
#include "certifywidget.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace GpgME;
using namespace Kleo;
CertifyCertificateDialog::CertifyCertificateDialog(QWidget *p, Qt::WindowFlags f)
: QDialog(p, f)
{
setWindowTitle(i18nc("@title:window", "Certify Certificates"));
setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint));
// Setup GUI
auto mainLay = new QVBoxLayout(this);
mCertWidget = new CertifyWidget(this);
mainLay->addWidget(mCertWidget);
auto buttonBox = new QDialogButtonBox{this};
buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
const auto okButton = buttonBox->button(QDialogButtonBox::Ok);
KGuiItem::assign(okButton, KStandardGuiItem::ok());
okButton->setText(i18n("Certify"));
KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
connect(buttonBox, &QDialogButtonBox::accepted, this, &CertifyCertificateDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
mainLay->addWidget(buttonBox);
okButton->setEnabled(mCertWidget->isValid());
connect(mCertWidget, &CertifyWidget::changed, this, [this, okButton]() {
okButton->setEnabled(mCertWidget->isValid());
});
KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("CertifyDialog"));
const auto size = cfgGroup.readEntry("Size", QSize{640, 480});
if (size.isValid()) {
resize(size);
}
}
CertifyCertificateDialog::~CertifyCertificateDialog()
{
KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("CertifyDialog"));
cfgGroup.writeEntry("Size", size());
cfgGroup.sync();
}
void CertifyCertificateDialog::setCertificateToCertify(const Key &key, const std::vector &uids)
{
- Q_ASSERT(Kleo::all_of(uids, [key](const auto &uid) {
+ Q_ASSERT(std::ranges::all_of(uids, [key](const auto &uid) {
return Kleo::userIDBelongsToKey(uid, key);
}));
setWindowTitle(i18nc("@title:window arg is name, email of certificate holder", "Certify Certificate: %1", Formatting::prettyName(key)));
mCertWidget->setCertificate(key, uids);
}
void CertifyCertificateDialog::setCertificatesToCertify(const std::vector &keys)
{
mCertWidget->setCertificates(keys);
}
void CertifyCertificateDialog::setGroupName(const QString &name)
{
setWindowTitle(i18nc("@title:window Certify Certificate Group ", "Certify Certificate Group %1", name));
}
bool CertifyCertificateDialog::exportableCertificationSelected() const
{
return mCertWidget->exportableSelected();
}
bool CertifyCertificateDialog::trustSignatureSelected() const
{
return mCertWidget->trustSignatureSelected();
}
QString CertifyCertificateDialog::trustSignatureDomain() const
{
return mCertWidget->trustSignatureDomain();
}
Key CertifyCertificateDialog::selectedSecretKey() const
{
return mCertWidget->secKey();
}
bool CertifyCertificateDialog::sendToServer() const
{
return mCertWidget->publishSelected();
}
void CertifyCertificateDialog::setSelectedUserIDs(const std::vector &uids)
{
mCertWidget->selectUserIDs(uids);
}
std::vector CertifyCertificateDialog::selectedUserIDs() const
{
return mCertWidget->selectedUserIDs();
}
QString CertifyCertificateDialog::tags() const
{
return mCertWidget->tags();
}
QDate CertifyCertificateDialog::expirationDate() const
{
return mCertWidget->expirationDate();
}
void CertifyCertificateDialog::accept()
{
if (!mCertWidget->isValid()) {
return;
}
mCertWidget->saveState();
QDialog::accept();
}
#include "moc_certifycertificatedialog.cpp"
diff --git a/src/dialogs/certifywidget.cpp b/src/dialogs/certifywidget.cpp
index 4b0b22e6e..7ea1f2d48 100644
--- a/src/dialogs/certifywidget.cpp
+++ b/src/dialogs/certifywidget.cpp
@@ -1,971 +1,971 @@
/* dialogs/certifywidget.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2019, 2021 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include
#include "certifywidget.h"
#include "dialogs/animatedexpander.h"
#include "view/infofield.h"
#include
#include
#include
#include
#include "kleopatra_debug.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
Q_DECLARE_METATYPE(GpgME::UserID)
using namespace Kleo;
using namespace GpgME;
static QDebug operator<<(QDebug s, const GpgME::UserID &userID)
{
return s << Formatting::prettyUserID(userID);
}
namespace
{
class SecKeyFilter : public DefaultKeyFilter
{
public:
SecKeyFilter()
: DefaultKeyFilter()
{
setRevoked(DefaultKeyFilter::NotSet);
setExpired(DefaultKeyFilter::NotSet);
setHasSecret(DefaultKeyFilter::Set);
setCanCertify(DefaultKeyFilter::Set);
setIsOpenPGP(DefaultKeyFilter::Set);
}
bool matches(const GpgME::Key &key, Kleo::KeyFilter::MatchContexts contexts) const override
{
if (!(availableMatchContexts() & contexts)) {
return false;
}
if (_detail::ByFingerprint()(key, mExcludedKey)) {
return false;
}
return DefaultKeyFilter::matches(key, contexts);
}
void setExcludedKey(const GpgME::Key &key)
{
mExcludedKey = key;
}
private:
GpgME::Key mExcludedKey;
};
auto checkBoxSize(const QCheckBox *checkBox)
{
QStyleOptionButton opt;
return checkBox->style()->sizeFromContents(QStyle::CT_CheckBox, &opt, QSize(), checkBox);
}
class TreeWidgetInternal : public TreeWidget
{
Q_OBJECT
public:
using TreeWidget::TreeWidget;
protected:
void focusInEvent(QFocusEvent *event) override
{
TreeWidget::focusInEvent(event);
// queue the invokation, so that it happens after the widget itself got focus
QMetaObject::invokeMethod(this, &TreeWidgetInternal::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection);
}
bool edit(const QModelIndex &index, EditTrigger trigger, QEvent *event) override
{
if (event && event->type() == QEvent::KeyPress) {
const auto *const keyEvent = static_cast(event);
if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Select) {
// toggle checked state regardless of the index's column
return TreeWidget::edit(index.siblingAtColumn(0), trigger, event);
}
}
return TreeWidget::edit(index, trigger, event);
}
private:
void forceAccessibleFocusEventForCurrentItem()
{
// force Qt to send a focus event for the current item to accessibility
// tools; otherwise, the user has no idea which item is selected when the
// list gets keyboard input focus
const auto current = currentIndex();
setCurrentIndex({});
setCurrentIndex(current);
}
};
struct UserIDCheckState {
GpgME::UserID userId;
Qt::CheckState checkState;
};
}
class CertifyWidget::Private
{
public:
enum Role { UserIdRole = Qt::UserRole };
enum Mode {
SingleCertification,
BulkCertification,
};
enum TagsState {
TagsMustBeChecked,
TagsLoading,
TagsLoaded,
};
Private(CertifyWidget *qq)
: q{qq}
{
auto mainLay = new QVBoxLayout{q};
{
mInfoLabel = new QLabel{i18n("Verify the fingerprint, mark the user IDs you want to certify, "
"and select the key you want to certify the user IDs with.
"
"Note: Only the fingerprint clearly identifies the key and its owner."),
q};
mInfoLabel->setWordWrap(true);
labelHelper.addLabel(mInfoLabel);
mainLay->addWidget(mInfoLabel);
}
mainLay->addWidget(new KSeparator{Qt::Horizontal, q});
{
auto grid = new QGridLayout;
grid->setColumnStretch(1, 1);
int row = -1;
row++;
mFprField = std::make_unique(i18n("Fingerprint:"), q);
grid->addWidget(mFprField->label(), row, 0);
grid->addLayout(mFprField->layout(), row, 1);
row++;
auto label = new QLabel{i18n("Certify with:"), q};
mSecKeySelect = new KeySelectionCombo{/* secretOnly= */ true, q};
mSecKeySelect->setKeyFilter(std::make_shared());
label->setBuddy(mSecKeySelect);
grid->addWidget(label, row, 0);
grid->addWidget(mSecKeySelect);
mainLay->addLayout(grid);
}
mMissingOwnerTrustInfo = new KMessageWidget{q};
mSetOwnerTrustAction = new QAction{q};
mSetOwnerTrustAction->setText(i18nc("@action:button", "Set Owner Trust"));
mSetOwnerTrustAction->setToolTip(i18nc("@info:tooltip",
"Click to set the trust level of the selected certification key to ultimate trust. "
"This is what you usually want to do for your own keys."));
connect(mSetOwnerTrustAction, &QAction::triggered, q, [this]() {
setOwnerTrust();
});
mMissingOwnerTrustInfo->addAction(mSetOwnerTrustAction);
mMissingOwnerTrustInfo->setVisible(false);
mainLay->addWidget(mMissingOwnerTrustInfo);
mainLay->addWidget(new KSeparator{Qt::Horizontal, q});
mBadCertificatesInfo = new KMessageWidget{q};
mBadCertificatesInfo->setMessageType(KMessageWidget::Warning);
mBadCertificatesInfo->setIcon(QIcon::fromTheme(QStringLiteral("data-warning"), QIcon::fromTheme(QStringLiteral("dialog-warning"))));
mBadCertificatesInfo->setText(i18nc("@info", "One or more certificates cannot be certified."));
mBadCertificatesInfo->setCloseButtonVisible(false);
mBadCertificatesInfo->setVisible(false);
mainLay->addWidget(mBadCertificatesInfo);
userIdListView = new TreeWidget{q};
userIdListView->setAccessibleName(i18n("User IDs"));
userIdListView->setEditTriggers(QAbstractItemView::NoEditTriggers);
userIdListView->setSelectionMode(QAbstractItemView::SingleSelection);
userIdListView->setRootIsDecorated(false);
userIdListView->setUniformRowHeights(true);
userIdListView->setAllColumnsShowFocus(false);
userIdListView->setHeaderHidden(true);
userIdListView->setHeaderLabels({i18nc("@title:column", "User ID")});
mainLay->addWidget(userIdListView, 1);
// Setup the advanced area
mAdvancedOptionsExpander = new AnimatedExpander{i18n("Advanced"), i18n("Show advanced options"), q};
mainLay->addWidget(mAdvancedOptionsExpander);
auto advLay = new QVBoxLayout;
mExportCB = new QCheckBox{q};
mExportCB->setText(i18n("Certify for everyone to see (exportable)"));
mExportCB->setToolTip(xi18nc("@info:tooltip",
"Check this option, if you want to share your certifications with others. "
"If you just want to mark certificates as certified for yourself, then you can uncheck it."));
advLay->addWidget(mExportCB);
{
auto layout = new QHBoxLayout;
mPublishCB = new QCheckBox{q};
mPublishCB->setText(i18n("Publish on keyserver afterwards"));
mPublishCB->setToolTip(xi18nc("@info:tooltip",
"Check this option, if you want to upload your certifications to a certificate "
"directory after successful certification."));
mPublishCB->setEnabled(mExportCB->isChecked());
layout->addSpacing(checkBoxSize(mExportCB).width());
layout->addWidget(mPublishCB);
advLay->addLayout(layout);
}
{
auto tagsLay = new QHBoxLayout;
auto label = new QLabel{i18n("Tags:"), q};
mTagsLE = new QLineEdit{q};
label->setBuddy(mTagsLE);
const auto tooltip = i18n("You can use this to add additional info to a certification.") + QStringLiteral("
")
+ i18n("Tags created by anyone with full certification trust "
"are shown in the keylist and can be searched.");
label->setToolTip(tooltip);
mTagsLE->setToolTip(tooltip);
tagsLay->addWidget(label);
tagsLay->addWidget(mTagsLE, 1);
advLay->addLayout(tagsLay);
}
{
auto layout = new QHBoxLayout;
mExpirationCheckBox = new QCheckBox{q};
mExpirationCheckBox->setText(i18n("Expiration:"));
mExpirationDateEdit = new KDateComboBox{q};
Kleo::setUpExpirationDateComboBox(mExpirationDateEdit, {QDate::currentDate().addDays(1), QDate{}});
mExpirationDateEdit->setDate(Kleo::defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration));
mExpirationDateEdit->setEnabled(mExpirationCheckBox->isChecked());
const auto tooltip = i18n("You can use this to set an expiration date for a certification.") + QStringLiteral("
")
+ i18n("By setting an expiration date, you can limit the validity of "
"your certification to a certain amount of time. Once the expiration "
"date has passed, your certification is no longer valid.");
mExpirationCheckBox->setToolTip(tooltip);
mExpirationDateEdit->setToolTip(tooltip);
layout->addWidget(mExpirationCheckBox);
layout->addWidget(mExpirationDateEdit, 1);
advLay->addLayout(layout);
}
{
mTrustSignatureCB = new QCheckBox{q};
mTrustSignatureWidgets.addWidget(mTrustSignatureCB);
mTrustSignatureCB->setText(i18n("Certify as trusted introducer"));
const auto tooltip = i18n("You can use this to certify a trusted introducer for a domain.") + QStringLiteral("
")
+ i18n("All certificates with email addresses belonging to the domain "
"that have been certified by the trusted introducer are treated "
"as certified, i.e. a trusted introducer acts as a kind of "
"intermediate CA for a domain.");
mTrustSignatureCB->setToolTip(tooltip);
advLay->addWidget(mTrustSignatureCB);
}
{
auto layout = new QHBoxLayout;
auto label = new QLabel{i18n("Domain:"), q};
mTrustSignatureWidgets.addWidget(label);
mTrustSignatureDomainLE = new QLineEdit{q};
mTrustSignatureWidgets.addWidget(mTrustSignatureDomainLE);
mTrustSignatureDomainLE->setEnabled(mTrustSignatureCB->isChecked());
label->setBuddy(mTrustSignatureDomainLE);
layout->addSpacing(checkBoxSize(mTrustSignatureCB).width());
layout->addWidget(label);
layout->addWidget(mTrustSignatureDomainLE);
advLay->addLayout(layout);
}
mAdvancedOptionsExpander->setContentLayout(advLay);
connect(userIdListView, &QTreeWidget::itemChanged, q, [this](auto item, auto) {
onItemChanged(item);
});
connect(mExportCB, &QCheckBox::toggled, q, [this](bool on) {
mPublishCB->setEnabled(on);
});
connect(mSecKeySelect, &KeySelectionCombo::currentKeyChanged, q, [this](const GpgME::Key &) {
updateSelectedUserIds();
updateTags();
checkOwnerTrust();
Q_EMIT q->changed();
});
connect(mExpirationCheckBox, &QCheckBox::toggled, q, [this](bool checked) {
mExpirationDateEdit->setEnabled(checked);
Q_EMIT q->changed();
});
connect(mExpirationDateEdit, &KDateComboBox::dateChanged, q, &CertifyWidget::changed);
connect(mTrustSignatureCB, &QCheckBox::toggled, q, [this](bool on) {
mTrustSignatureDomainLE->setEnabled(on);
Q_EMIT q->changed();
});
connect(mTrustSignatureDomainLE, &QLineEdit::textChanged, q, &CertifyWidget::changed);
loadConfig(true);
}
~Private() = default;
void loadConfig(bool loadAll = false)
{
const KConfigGroup conf(KSharedConfig::openConfig(), QStringLiteral("CertifySettings"));
if (loadAll) {
const Settings settings;
mExpirationCheckBox->setChecked(settings.certificationValidityInDays() > 0);
if (settings.certificationValidityInDays() > 0) {
const QDate expirationDate = QDate::currentDate().addDays(settings.certificationValidityInDays());
mExpirationDateEdit->setDate(expirationDate > mExpirationDateEdit->maximumDate() //
? mExpirationDateEdit->maximumDate() //
: expirationDate);
}
mSecKeySelect->setDefaultKey(conf.readEntry("LastKey", QString()));
}
switch (mMode) {
case SingleCertification: {
mExportCB->setChecked(conf.readEntry("ExportCheckState", false));
mPublishCB->setChecked(conf.readEntry("PublishCheckState", false));
mAdvancedOptionsExpander->setExpanded(conf.readEntry("AdvancedOptionsExpanded", false));
break;
}
case BulkCertification: {
mExportCB->setChecked(conf.readEntry("BulkExportCheckState", true));
mPublishCB->setChecked(conf.readEntry("BulkPublishCheckState", false));
mAdvancedOptionsExpander->setExpanded(conf.readEntry("BulkAdvancedOptionsExpanded", true));
break;
}
}
}
void saveConfig()
{
KConfigGroup conf{KSharedConfig::openConfig(), QLatin1StringView("CertifySettings")};
if (!secKey().isNull()) {
conf.writeEntry("LastKey", secKey().primaryFingerprint());
}
switch (mMode) {
case SingleCertification: {
conf.writeEntry("ExportCheckState", mExportCB->isChecked());
conf.writeEntry("PublishCheckState", mPublishCB->isChecked());
conf.writeEntry("AdvancedOptionsExpanded", mAdvancedOptionsExpander->isExpanded());
break;
}
case BulkCertification: {
conf.writeEntry("BulkExportCheckState", mExportCB->isChecked());
conf.writeEntry("BulkPublishCheckState", mPublishCB->isChecked());
conf.writeEntry("BulkAdvancedOptionsExpanded", mAdvancedOptionsExpander->isExpanded());
break;
}
}
conf.sync();
}
void setMode(Mode mode)
{
mMode = mode;
switch (mMode) {
case SingleCertification:
break;
case BulkCertification: {
mInfoLabel->setText(i18nc("@info",
"Verify the fingerprints, mark the user IDs you want to certify, "
"and select the certificate you want to certify the user IDs with.
"
"Note: Only the fingerprints clearly identify the certificate and its owner."));
mFprField->setVisible(false);
mTrustSignatureWidgets.setVisible(false);
break;
}
}
loadConfig();
}
void setUpUserIdList(const std::vector &uids = {})
{
userIdListView->clear();
if (mMode == SingleCertification) {
userIdListView->setColumnCount(1);
userIdListView->setHeaderHidden(true);
// set header labels for accessibility tools to overwrite the default "1"
userIdListView->setHeaderLabels({i18nc("@title:column", "User ID")});
for (const auto &uid : uids) {
if (uid.isInvalid() || Kleo::isRevokedOrExpired(uid)) {
// Skip user IDs that cannot really be certified.
continue;
}
auto item = new QTreeWidgetItem;
item->setData(0, UserIdRole, QVariant::fromValue(uid));
item->setData(0, Qt::DisplayRole, Kleo::Formatting::prettyUserID(uid));
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
item->setCheckState(0, Qt::Checked);
userIdListView->addTopLevelItem(item);
}
} else {
const QStringList headers = {i18nc("@title:column", "User ID"), i18nc("@title:column", "Fingerprint")};
userIdListView->setColumnCount(headers.count());
userIdListView->setHeaderHidden(false);
userIdListView->setHeaderLabels(headers);
for (const auto &key : mKeys) {
const auto &uid = key.userID(0);
auto item = new QTreeWidgetItem;
item->setData(0, UserIdRole, QVariant::fromValue(uid));
item->setData(0, Qt::DisplayRole, Kleo::Formatting::prettyUserID(uid));
item->setData(1, Qt::DisplayRole, Kleo::Formatting::prettyID(key.primaryFingerprint()));
item->setData(1, Qt::AccessibleTextRole, Kleo::Formatting::accessibleHexID(key.primaryFingerprint()));
if ((key.protocol() != OpenPGP) || uid.isInvalid() || Kleo::isRevokedOrExpired(uid)) {
item->setFlags(Qt::NoItemFlags);
item->setCheckState(0, Qt::Unchecked);
if (key.protocol() == CMS) {
item->setData(0, Qt::ToolTipRole, i18nc("@info:tooltip", "S/MIME certificates cannot be certified."));
item->setData(1, Qt::ToolTipRole, i18nc("@info:tooltip", "S/MIME certificates cannot be certified."));
} else {
item->setData(0, Qt::ToolTipRole, i18nc("@info:tooltip", "Expired or revoked certificates cannot be certified."));
item->setData(1, Qt::ToolTipRole, i18nc("@info:tooltip", "Expired or revoked certificates cannot be certified."));
}
} else {
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
item->setCheckState(0, Qt::Checked);
}
userIdListView->addTopLevelItem(item);
}
userIdListView->sortItems(0, Qt::AscendingOrder);
userIdListView->resizeColumnToContents(0);
userIdListView->resizeColumnToContents(1);
}
}
void updateSelectedUserIds()
{
if (mMode == SingleCertification) {
return;
}
if (userIdListView->topLevelItemCount() == 0) {
return;
}
// restore check state of primary user ID of previous certification key
if (!mCertificationKey.isNull()) {
for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
const auto uidItem = userIdListView->topLevelItem(i);
const auto itemUserId = getUserId(uidItem);
if (userIDBelongsToKey(itemUserId, mCertificationKey)) {
uidItem->setCheckState(0, mCertificationKeyUserIDCheckState);
uidItem->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
break; // we only show the primary user IDs
}
}
}
mCertificationKey = mSecKeySelect->currentKey();
// save and unset check state of primary user ID of current certification key
if (!mCertificationKey.isNull()) {
for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
const auto uidItem = userIdListView->topLevelItem(i);
const auto itemUserId = getUserId(uidItem);
if (userIDBelongsToKey(itemUserId, mCertificationKey)) {
mCertificationKeyUserIDCheckState = uidItem->checkState(0);
if (mCertificationKeyUserIDCheckState) {
uidItem->setCheckState(0, Qt::Unchecked);
}
uidItem->setFlags(Qt::ItemIsSelectable);
break; // we only show the primary user IDs
}
}
}
}
void updateTags()
{
struct ItemAndRemark {
QTreeWidgetItem *item;
QString remark;
};
if (mTagsState != TagsLoaded) {
return;
}
if (mTagsLE->isModified()) {
return;
}
GpgME::Key remarkKey = mSecKeySelect->currentKey();
if (!remarkKey.isNull()) {
std::vector itemsAndRemarks;
// first choose the remark we want to prefill the Tags field with
QString remark;
for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
const auto item = userIdListView->topLevelItem(i);
if (item->isDisabled()) {
continue;
}
const auto uid = getUserId(item);
GpgME::Error err;
const char *c_remark = uid.remark(remarkKey, err);
const QString itemRemark = (!err && c_remark) ? QString::fromUtf8(c_remark) : QString{};
if (!itemRemark.isEmpty() && (itemRemark != remark)) {
if (!remark.isEmpty()) {
qCDebug(KLEOPATRA_LOG) << "Different remarks on user IDs. Taking last.";
}
remark = itemRemark;
}
itemsAndRemarks.push_back({item, itemRemark});
}
// then select the user IDs with the chosen remark; this prevents overwriting existing
// different remarks on the other user IDs (as long as the user doesn't select any of
// the unselected user IDs with a different remark)
if (!remark.isEmpty()) {
for (const auto &[item, itemRemark] : itemsAndRemarks) {
item->setCheckState(0, itemRemark == remark ? Qt::Checked : Qt::Unchecked);
}
}
mTagsLE->setText(remark);
}
}
void updateTrustSignatureDomain()
{
if (mMode == SingleCertification) {
if (mTrustSignatureDomainLE->text().isEmpty() && certificate().numUserIDs() == 1) {
// try to guess the domain to use for the trust signature
const auto address = certificate().userID(0).addrSpec();
const auto atPos = address.find('@');
if (atPos != std::string::npos) {
const auto domain = address.substr(atPos + 1);
mTrustSignatureDomainLE->setText(QString::fromUtf8(domain.c_str(), domain.size()));
}
}
}
}
void loadAllTags()
{
const auto keyWithoutTags = std::find_if(mKeys.cbegin(), mKeys.cend(), [](const auto &key) {
return (key.protocol() == GpgME::OpenPGP) && !(key.keyListMode() & GpgME::SignatureNotations);
});
const auto indexOfKeyWithoutTags = std::distance(mKeys.cbegin(), keyWithoutTags);
if (indexOfKeyWithoutTags < signed(mKeys.size())) {
auto loadTags = [this, indexOfKeyWithoutTags]() {
Q_ASSERT(indexOfKeyWithoutTags < signed(mKeys.size()));
// call update() on the reference to the vector element because it swaps key with the updated key
mKeys[indexOfKeyWithoutTags].update();
loadAllTags();
};
QMetaObject::invokeMethod(q, loadTags, Qt::QueuedConnection);
return;
}
mTagsState = TagsLoaded;
QMetaObject::invokeMethod(
q,
[this]() {
setUpWidget();
},
Qt::QueuedConnection);
}
bool ensureTagsLoaded()
{
Q_ASSERT(mTagsState != TagsLoading);
if (mTagsState == TagsLoaded) {
return true;
}
- const auto allTagsAreLoaded = Kleo::all_of(mKeys, [](const auto &key) {
+ const auto allTagsAreLoaded = std::ranges::all_of(mKeys, [](const auto &key) {
return (key.protocol() != GpgME::OpenPGP) || (key.keyListMode() & GpgME::SignatureNotations);
});
if (allTagsAreLoaded) {
mTagsState = TagsLoaded;
} else {
mTagsState = TagsLoading;
QMetaObject::invokeMethod(
q,
[this]() {
loadAllTags();
},
Qt::QueuedConnection);
}
return mTagsState == TagsLoaded;
}
void setUpWidget()
{
if (!ensureTagsLoaded()) {
return;
}
if (mMode == SingleCertification) {
const auto key = certificate();
mFprField->setValue(QStringLiteral("") + Formatting::prettyID(key.primaryFingerprint()) + QStringLiteral(""),
Formatting::accessibleHexID(key.primaryFingerprint()));
setUpUserIdList(mUserIds.empty() ? key.userIDs() : mUserIds);
auto keyFilter = std::make_shared();
keyFilter->setExcludedKey(key);
mSecKeySelect->setKeyFilter(keyFilter);
updateTrustSignatureDomain();
} else {
// check for certificates that cannot be certified
- const auto haveBadCertificates = Kleo::any_of(mKeys, [](const auto &key) {
+ const auto haveBadCertificates = std::ranges::any_of(mKeys, [](const auto &key) {
const auto &uid = key.userID(0);
return (key.protocol() != OpenPGP) || uid.isInvalid() || Kleo::isRevokedOrExpired(uid);
});
if (haveBadCertificates) {
mBadCertificatesInfo->animatedShow();
}
setUpUserIdList();
}
updateTags();
updateSelectedUserIds();
Q_EMIT q->changed();
}
GpgME::Key certificate() const
{
Q_ASSERT(mMode == SingleCertification);
return !mKeys.empty() ? mKeys.front() : Key{};
}
void setCertificates(const std::vector &keys, const std::vector &uids)
{
mKeys = keys;
mUserIds = uids;
mTagsState = TagsMustBeChecked;
setUpWidget();
}
std::vector certificates() const
{
Q_ASSERT(mMode != SingleCertification);
return mKeys;
}
GpgME::Key secKey() const
{
return mSecKeySelect->currentKey();
}
GpgME::UserID getUserId(const QTreeWidgetItem *item) const
{
return item ? item->data(0, UserIdRole).value() : UserID{};
}
void selectUserIDs(const std::vector &uids)
{
for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
const auto uidItem = userIdListView->topLevelItem(i);
const auto itemUserId = getUserId(uidItem);
- const bool userIdIsInList = Kleo::any_of(uids, [itemUserId](const auto &uid) {
+ const bool userIdIsInList = std::ranges::any_of(uids, [itemUserId](const auto &uid) {
return Kleo::userIDsAreEqual(itemUserId, uid);
});
uidItem->setCheckState(0, userIdIsInList ? Qt::Checked : Qt::Unchecked);
}
}
std::vector selectedUserIDs() const
{
std::vector userIds;
userIds.reserve(userIdListView->topLevelItemCount());
for (int i = 0, end = userIdListView->topLevelItemCount(); i < end; ++i) {
const auto *const uidItem = userIdListView->topLevelItem(i);
if (uidItem->checkState(0) == Qt::Checked) {
userIds.push_back(getUserId(uidItem));
}
}
qCDebug(KLEOPATRA_LOG) << "Checked user IDs:" << userIds;
return userIds;
}
bool exportableSelected() const
{
return mExportCB->isChecked();
}
bool publishSelected() const
{
return mPublishCB->isChecked();
}
QString tags() const
{
return mTagsLE->text().trimmed();
}
bool isValid() const
{
static const QRegularExpression domainNameRegExp{QStringLiteral(R"(^\s*((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}\s*$)"),
QRegularExpression::CaseInsensitiveOption};
if (mTagsState != TagsLoaded) {
return false;
}
// do not accept null keys
if (mKeys.empty() || mSecKeySelect->currentKey().isNull()) {
return false;
}
// do not accept empty list of user IDs
const auto userIds = selectedUserIDs();
if (userIds.empty()) {
return false;
}
// do not accept if any of the selected user IDs belongs to the certification key
const auto certificationKey = mSecKeySelect->currentKey();
const auto userIdToCertifyBelongsToCertificationKey = std::any_of(userIds.cbegin(), userIds.cend(), [certificationKey](const auto &userId) {
return Kleo::userIDBelongsToKey(userId, certificationKey);
});
if (userIdToCertifyBelongsToCertificationKey) {
return false;
}
if (mExpirationCheckBox->isChecked() && !mExpirationDateEdit->isValid()) {
return false;
}
if (mTrustSignatureCB->isChecked() && !domainNameRegExp.match(mTrustSignatureDomainLE->text()).hasMatch()) {
return false;
}
return true;
}
void checkOwnerTrust()
{
const auto secretKey = secKey();
if (secretKey.ownerTrust() != GpgME::Key::Ultimate) {
mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Information);
mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("question")));
mMissingOwnerTrustInfo->setText(i18n("Is this your own key?"));
mSetOwnerTrustAction->setEnabled(true);
mMissingOwnerTrustInfo->animatedShow();
} else {
mMissingOwnerTrustInfo->animatedHide();
}
}
void setOwnerTrust()
{
mSetOwnerTrustAction->setEnabled(false);
QGpgME::ChangeOwnerTrustJob *const j = QGpgME::openpgp()->changeOwnerTrustJob();
connect(j, &QGpgME::ChangeOwnerTrustJob::result, q, [this](const GpgME::Error &err) {
if (err) {
KMessageBox::error(q,
i18n("Changing the certification trust of the key %1 failed:
%2
",
Formatting::formatForComboBox(secKey()),
Formatting::errorAsString(err)),
i18nc("@title:window", "Certification Trust Change Failed"));
}
if (err || err.isCanceled()) {
mSetOwnerTrustAction->setEnabled(true);
} else {
mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Positive);
mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("checkmark")));
mMissingOwnerTrustInfo->setText(i18n("Owner trust set successfully."));
}
});
j->start(secKey(), GpgME::Key::Ultimate);
}
void onItemChanged(QTreeWidgetItem *item)
{
Q_EMIT q->changed();
#ifndef QT_NO_ACCESSIBILITY
if (item) {
// assume that the checked state changed
QAccessible::State st;
st.checked = true;
QAccessibleStateChangeEvent e(userIdListView, st);
e.setChild(userIdListView->indexOfTopLevelItem(item));
QAccessible::updateAccessibility(&e);
}
#endif
}
public:
CertifyWidget *const q;
QLabel *mInfoLabel = nullptr;
std::unique_ptr mFprField;
KeySelectionCombo *mSecKeySelect = nullptr;
KMessageWidget *mMissingOwnerTrustInfo = nullptr;
KMessageWidget *mBadCertificatesInfo = nullptr;
TreeWidget *userIdListView = nullptr;
AnimatedExpander *mAdvancedOptionsExpander = nullptr;
QCheckBox *mExportCB = nullptr;
QCheckBox *mPublishCB = nullptr;
QLineEdit *mTagsLE = nullptr;
BulkStateChanger mTrustSignatureWidgets;
QCheckBox *mTrustSignatureCB = nullptr;
QLineEdit *mTrustSignatureDomainLE = nullptr;
QCheckBox *mExpirationCheckBox = nullptr;
KDateComboBox *mExpirationDateEdit = nullptr;
QAction *mSetOwnerTrustAction = nullptr;
LabelHelper labelHelper;
Mode mMode = SingleCertification;
std::vector mKeys;
std::vector mUserIds;
TagsState mTagsState = TagsMustBeChecked;
GpgME::Key mCertificationKey;
Qt::CheckState mCertificationKeyUserIDCheckState;
};
CertifyWidget::CertifyWidget(QWidget *parent)
: QWidget{parent}
, d{std::make_unique(this)}
{
}
Kleo::CertifyWidget::~CertifyWidget() = default;
void CertifyWidget::setCertificate(const GpgME::Key &key, const std::vector &uids)
{
Q_ASSERT(!key.isNull());
d->setMode(Private::SingleCertification);
d->setCertificates({key}, uids);
}
GpgME::Key CertifyWidget::certificate() const
{
return d->certificate();
}
void CertifyWidget::setCertificates(const std::vector &keys)
{
d->setMode(Private::BulkCertification);
d->setCertificates(keys, {});
}
std::vector CertifyWidget::certificates() const
{
return d->certificates();
}
void CertifyWidget::selectUserIDs(const std::vector &uids)
{
d->selectUserIDs(uids);
}
std::vector