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 #include #include #include using namespace GpgME; using namespace Kleo; using namespace QGpgME; static void disconnectConnection(const QMetaObject::Connection &connection) { // trivial function for disconnecting a signal-slot connection QObject::disconnect(connection); } bool operator==(const ImportJobData &lhs, const ImportJobData &rhs) { return lhs.job == rhs.job; } namespace { make_comparator_str(ByImportFingerprint, .fingerprint()); class ImportResultProxyModel : public AbstractKeyListSortFilterProxyModel { Q_OBJECT public: ImportResultProxyModel(const std::vector &results, QObject *parent = nullptr) : AbstractKeyListSortFilterProxyModel(parent) { updateFindCache(results); } ~ImportResultProxyModel() override { } ImportResultProxyModel *clone() const override { // compiler-generated copy ctor is fine! return new ImportResultProxyModel(*this); } void setImportResults(const std::vector &results) { updateFindCache(results); invalidateFilter(); } protected: QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid() || role != Qt::ToolTipRole) { return AbstractKeyListSortFilterProxyModel::data(index, role); } const QString fpr = index.data(KeyList::FingerprintRole).toString(); // find information: const std::vector::const_iterator it = Kleo::binary_find(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint()); if (it == m_importsByFingerprint.end()) { return AbstractKeyListSortFilterProxyModel::data(index, role); } else { QStringList rv; const auto ids = m_idsByFingerprint[it->fingerprint()]; rv.reserve(ids.size()); std::copy(ids.cbegin(), ids.cend(), std::back_inserter(rv)); return Formatting::importMetaData(*it, rv); } } bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { // // 0. Keep parents of matching children: // const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); Q_ASSERT(index.isValid()); for (int i = 0, end = sourceModel()->rowCount(index); i != end; ++i) if (filterAcceptsRow(i, index)) { return true; } // // 1. Check that this is an imported key: // const QString fpr = index.data(KeyList::FingerprintRole).toString(); return std::binary_search(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint()); } private: void updateFindCache(const std::vector &results) { m_importsByFingerprint.clear(); m_idsByFingerprint.clear(); m_results = results; for (const auto &r : results) { const std::vector imports = r.result.imports(); m_importsByFingerprint.insert(m_importsByFingerprint.end(), imports.begin(), imports.end()); for (std::vector::const_iterator it = imports.begin(), end = imports.end(); it != end; ++it) { m_idsByFingerprint[it->fingerprint()].insert(r.id); } } std::sort(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), ByImportFingerprint()); } private: mutable std::vector m_importsByFingerprint; mutable std::map, ByImportFingerprint> m_idsByFingerprint; std::vector m_results; }; bool importFailed(const ImportResultData &r) { // ignore GPG_ERR_EOF error to handle the "failed" import of files // without X.509 certificates by gpgsm gracefully return r.result.error() && r.result.error().code() != GPG_ERR_EOF; } bool importWasCanceled(const ImportResultData &r) { return r.result.error().isCanceled(); } } ImportCertificatesCommand::Private::Private(ImportCertificatesCommand *qq, KeyListController *c) : Command::Private(qq, c) , progressWindowTitle{i18nc("@title:window", "Importing Certificates")} , progressLabelText{i18n("Importing certificates... (this can take a while)")} { } ImportCertificatesCommand::Private::~Private() { if (progressDialog) { delete progressDialog; } } #define d d_func() #define q q_func() ImportCertificatesCommand::ImportCertificatesCommand(KeyListController *p) : Command(new Private(this, p)) { } ImportCertificatesCommand::ImportCertificatesCommand(QAbstractItemView *v, KeyListController *p) : Command(v, new Private(this, p)) { } ImportCertificatesCommand::~ImportCertificatesCommand() = default; static QString format_ids(const std::vector &ids) { QStringList escapedIds; for (const QString &id : ids) { if (!id.isEmpty()) { escapedIds << id.toHtmlEscaped(); } } return escapedIds.join(QLatin1StringView("
")); } static QString make_tooltip(const std::vector &results) { if (results.empty()) { return {}; } std::vector ids; ids.reserve(results.size()); std::transform(std::begin(results), std::end(results), std::back_inserter(ids), [](const auto &r) { return r.id; }); std::sort(std::begin(ids), std::end(ids)); ids.erase(std::unique(std::begin(ids), std::end(ids)), std::end(ids)); if (ids.size() == 1) if (ids.front().isEmpty()) { return {}; } else return i18nc("@info:tooltip", "Imported Certificates from %1", ids.front().toHtmlEscaped()); else return i18nc("@info:tooltip", "Imported certificates from these sources:
%1", format_ids(ids)); } void ImportCertificatesCommand::Private::setImportResultProxyModel(const std::vector &results) { if (std::none_of(std::begin(results), std::end(results), [](const auto &r) { return r.result.numConsidered() > 0; })) { return; } q->addTemporaryView(i18nc("@title:tab", "Imported Certificates"), new ImportResultProxyModel(results), make_tooltip(results)); if (QTreeView *const tv = qobject_cast(parentWidgetOrView())) { tv->expandAll(); } } int sum(const std::vector &res, int (ImportResult::*fun)() const) { return kdtools::accumulate_transform(res.begin(), res.end(), std::mem_fn(fun), 0); } static QString make_report(const std::vector &results, const std::vector &groups) { const KLocalizedString normalLine = ki18n("%1%2"); const KLocalizedString boldLine = ki18n("%1%2"); const KLocalizedString headerLine = ki18n("%1"); std::vector res; res.reserve(results.size()); std::transform(std::begin(results), std::end(results), std::back_inserter(res), [](const auto &r) { return r.result; }); const auto numProcessedCertificates = sum(res, &ImportResult::numConsidered); QStringList lines; if (numProcessedCertificates > 0 || groups.size() == 0) { lines.push_back(headerLine.subs(i18n("Certificates")).toString()); lines.push_back(normalLine.subs(i18n("Total number processed:")).subs(numProcessedCertificates).toString()); lines.push_back(normalLine.subs(i18n("Imported:")).subs(sum(res, &ImportResult::numImported)).toString()); if (const int n = sum(res, &ImportResult::newSignatures)) lines.push_back(normalLine.subs(i18n("New signatures:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::newUserIDs)) lines.push_back(normalLine.subs(i18n("New user IDs:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::numKeysWithoutUserID)) lines.push_back(normalLine.subs(i18n("Certificates without user IDs:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::newSubkeys)) lines.push_back(normalLine.subs(i18n("New subkeys:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::newRevocations)) lines.push_back(boldLine.subs(i18n("Newly revoked:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::notImported)) lines.push_back(boldLine.subs(i18n("Not imported:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::numUnchanged)) lines.push_back(normalLine.subs(i18n("Unchanged:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysConsidered)) lines.push_back(normalLine.subs(i18n("Secret keys processed:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysImported)) lines.push_back(normalLine.subs(i18n("Secret keys imported:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysConsidered) - sum(res, &ImportResult::numSecretKeysImported) - sum(res, &ImportResult::numSecretKeysUnchanged)) if (n > 0) lines.push_back(boldLine.subs(i18n("Secret keys not imported:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysUnchanged)) lines.push_back(normalLine.subs(i18n("Secret keys unchanged:")).subs(n).toString()); if (const int n = sum(res, &ImportResult::numV3KeysSkipped)) lines.push_back(normalLine.subs(i18n("Deprecated PGP-2 keys skipped:")).subs(n).toString()); } if (!lines.empty()) { lines.push_back(headerLine.subs(QLatin1StringView{" "}).toString()); } if (groups.size() > 0) { const auto newGroups = std::count_if(std::begin(groups), std::end(groups), [](const auto &g) { return g.status == ImportedGroup::Status::New; }); const auto updatedGroups = groups.size() - newGroups; lines.push_back(headerLine.subs(i18n("Certificate Groups")).toString()); lines.push_back(normalLine.subs(i18n("Total number processed:")).subs(groups.size()).toString()); lines.push_back(normalLine.subs(i18n("New groups:")).subs(newGroups).toString()); lines.push_back(normalLine.subs(i18n("Updated groups:")).subs(updatedGroups).toString()); } return lines.join(QLatin1StringView{}); } static bool isImportFromSingleSource(const std::vector &res) { return (res.size() == 1) || (res.size() == 2 && res[0].id == res[1].id); } static QString make_message_report(const std::vector &res, const std::vector &groups) { QString report{QLatin1StringView{""}}; if (res.empty()) { report += i18n("No imports (should not happen, please report a bug)."); } else { const QString title = isImportFromSingleSource(res) && !res.front().id.isEmpty() ? i18n("Detailed results of importing %1:", res.front().id) : i18n("Detailed results of import:"); report += QLatin1StringView{"

"} + title + QLatin1String{"

"}; report += QLatin1StringView{"

"}; report += make_report(res, groups); report += QLatin1StringView{"

"}; } 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("
      "))) + 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 CertifyWidget::selectedUserIDs() const { return d->selectedUserIDs(); } GpgME::Key CertifyWidget::secKey() const { return d->secKey(); } bool CertifyWidget::exportableSelected() const { return d->exportableSelected(); } QString CertifyWidget::tags() const { return d->tags(); } bool CertifyWidget::publishSelected() const { return d->publishSelected(); } bool CertifyWidget::trustSignatureSelected() const { return d->mTrustSignatureCB->isChecked(); } QString CertifyWidget::trustSignatureDomain() const { return d->mTrustSignatureDomainLE->text().trimmed(); } QDate CertifyWidget::expirationDate() const { return d->mExpirationCheckBox->isChecked() ? d->mExpirationDateEdit->date() : QDate{}; } bool CertifyWidget::isValid() const { return d->isValid(); } void CertifyWidget::saveState() const { d->saveConfig(); } #include "certifywidget.moc" #include "moc_certifywidget.cpp" diff --git a/src/dialogs/editgroupdialog.cpp b/src/dialogs/editgroupdialog.cpp index 949edf5cf..f7e21cf5b 100644 --- a/src/dialogs/editgroupdialog.cpp +++ b/src/dialogs/editgroupdialog.cpp @@ -1,487 +1,487 @@ /* dialogs/editgroupdialog.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 "editgroupdialog.h" #include "commands/detailscommand.h" #include "utils/gui-helper.h" #include "view/keytreeview.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 "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) namespace { auto createOpenPGPOnlyKeyFilter() { auto filter = std::make_shared(); filter->setIsOpenPGP(DefaultKeyFilter::Set); return filter; } } class WarnNonEncryptionKeysProxyModel : public Kleo::AbstractKeyListSortFilterProxyModel { Q_OBJECT public: using Kleo::AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel; ~WarnNonEncryptionKeysProxyModel() override; WarnNonEncryptionKeysProxyModel *clone() const override { return new WarnNonEncryptionKeysProxyModel(this->parent()); } QVariant data(const QModelIndex &index, int role) const override { const auto sourceIndex = sourceModel()->index(index.row(), index.column()); if (!Kleo::keyHasEncrypt(sourceIndex.data(KeyList::KeyRole).value())) { if (role == Qt::DecorationRole && index.column() == 0) { return QIcon::fromTheme(QStringLiteral("data-warning")); } if (role == Qt::ToolTipRole) { return i18nc("@info:tooltip", "This certificate cannot be used for encryption."); } } return sourceIndex.data(role); } }; WarnNonEncryptionKeysProxyModel::~WarnNonEncryptionKeysProxyModel() = default; class DisableNonEncryptionKeysProxyModel : public Kleo::AbstractKeyListSortFilterProxyModel { Q_OBJECT public: using Kleo::AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel; ~DisableNonEncryptionKeysProxyModel() override; DisableNonEncryptionKeysProxyModel *clone() const override { return new DisableNonEncryptionKeysProxyModel(this->parent()); } QVariant data(const QModelIndex &index, int role) const override { const auto sourceIndex = sourceModel()->index(index.row(), index.column()); if (!Kleo::keyHasEncrypt(sourceIndex.data(KeyList::KeyRole).value())) { if (role == Qt::ForegroundRole) { return qApp->palette().color(QPalette::Disabled, QPalette::Text); } if (role == Qt::BackgroundRole) { return KColorScheme(QPalette::Disabled, KColorScheme::View).background(KColorScheme::NeutralBackground).color(); } if (role == Qt::ToolTipRole) { return i18nc("@info:tooltip", "This certificate cannot be added to the group as it cannot be used for encryption."); } } return sourceIndex.data(role); } Qt::ItemFlags flags(const QModelIndex &index) const override { auto originalFlags = index.model()->QAbstractItemModel::flags(index); if (Kleo::keyHasEncrypt(index.data(KeyList::KeyRole).value())) { return originalFlags; } else { return (originalFlags & ~Qt::ItemIsEnabled); } return {}; } }; DisableNonEncryptionKeysProxyModel::~DisableNonEncryptionKeysProxyModel() = default; class EditGroupDialog::Private { friend class ::Kleo::Dialogs::EditGroupDialog; EditGroupDialog *const q; struct { QLineEdit *groupNameEdit = nullptr; QLineEdit *availableKeysFilter = nullptr; KeyTreeView *availableKeysList = nullptr; QLineEdit *groupKeysFilter = nullptr; KeyTreeView *groupKeysList = nullptr; QDialogButtonBox *buttonBox = nullptr; } ui; AbstractKeyListModel *availableKeysModel = nullptr; AbstractKeyListModel *groupKeysModel = nullptr; public: Private(EditGroupDialog *qq) : q(qq) { auto mainLayout = new QVBoxLayout(q); { auto groupNameLayout = new QHBoxLayout(); auto label = new QLabel(i18nc("Name of a group of keys", "Name:"), q); groupNameLayout->addWidget(label); ui.groupNameEdit = new QLineEdit(q); label->setBuddy(ui.groupNameEdit); groupNameLayout->addWidget(ui.groupNameEdit); mainLayout->addLayout(groupNameLayout); } mainLayout->addWidget(new KSeparator(Qt::Horizontal, q)); auto centerLayout = new QVBoxLayout; auto availableKeysGroupBox = new QGroupBox{i18nc("@title", "Available Keys"), q}; availableKeysGroupBox->setFlat(true); auto availableKeysLayout = new QVBoxLayout{availableKeysGroupBox}; { auto hbox = new QHBoxLayout; auto label = new QLabel{i18nc("@label", "Search:")}; label->setAccessibleName(i18nc("@label", "Search available keys")); label->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term.")); hbox->addWidget(label); ui.availableKeysFilter = new QLineEdit(q); ui.availableKeysFilter->setClearButtonEnabled(true); ui.availableKeysFilter->setAccessibleName(i18nc("@label", "Search available keys")); ui.availableKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term.")); ui.availableKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term")); ui.availableKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event label->setBuddy(ui.availableKeysFilter); hbox->addWidget(ui.availableKeysFilter, 1); availableKeysLayout->addLayout(hbox); } availableKeysModel = AbstractKeyListModel::createFlatKeyListModel(q); availableKeysModel->setKeys(KeyCache::instance()->keys()); auto proxyModel = new DisableNonEncryptionKeysProxyModel(q); proxyModel->setSourceModel(availableKeysModel); ui.availableKeysList = new KeyTreeView({}, nullptr, proxyModel, q, {}); ui.availableKeysList->view()->setAccessibleName(i18n("available keys")); ui.availableKeysList->view()->setRootIsDecorated(false); ui.availableKeysList->setFlatModel(availableKeysModel); ui.availableKeysList->setHierarchicalView(false); if (!Settings{}.cmsEnabled()) { ui.availableKeysList->setKeyFilter(createOpenPGPOnlyKeyFilter()); } availableKeysLayout->addWidget(ui.availableKeysList, /*stretch=*/1); centerLayout->addWidget(availableKeysGroupBox, /*stretch=*/1); auto buttonsLayout = new QHBoxLayout; buttonsLayout->addStretch(1); auto addButton = new QPushButton(q); addButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down"))); addButton->setAccessibleName(i18nc("@action:button", "Add Selected Keys")); addButton->setToolTip(i18n("Add the selected keys to the group")); addButton->setEnabled(false); buttonsLayout->addWidget(addButton); auto removeButton = new QPushButton(q); removeButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up"))); removeButton->setAccessibleName(i18nc("@action:button", "Remove Selected Keys")); removeButton->setToolTip(i18n("Remove the selected keys from the group")); removeButton->setEnabled(false); buttonsLayout->addWidget(removeButton); buttonsLayout->addStretch(1); centerLayout->addLayout(buttonsLayout); auto groupKeysGroupBox = new QGroupBox{i18nc("@title", "Group Keys"), q}; groupKeysGroupBox->setFlat(true); auto groupKeysLayout = new QVBoxLayout{groupKeysGroupBox}; { auto hbox = new QHBoxLayout; auto label = new QLabel{i18nc("@label", "Search:")}; label->setAccessibleName(i18nc("@label", "Search group keys")); label->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term.")); hbox->addWidget(label); ui.groupKeysFilter = new QLineEdit(q); ui.groupKeysFilter->setClearButtonEnabled(true); ui.groupKeysFilter->setAccessibleName(i18nc("@label", "Search group keys")); ui.groupKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term.")); ui.groupKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term")); ui.groupKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event label->setBuddy(ui.groupKeysFilter); hbox->addWidget(ui.groupKeysFilter, 1); groupKeysLayout->addLayout(hbox); } groupKeysModel = AbstractKeyListModel::createFlatKeyListModel(q); auto warnNonEncryptionProxyModel = new WarnNonEncryptionKeysProxyModel(q); ui.groupKeysList = new KeyTreeView({}, nullptr, warnNonEncryptionProxyModel, q, {}); ui.groupKeysList->view()->setAccessibleName(i18n("group keys")); ui.groupKeysList->view()->setRootIsDecorated(false); ui.groupKeysList->setFlatModel(groupKeysModel); ui.groupKeysList->setHierarchicalView(false); groupKeysLayout->addWidget(ui.groupKeysList, /*stretch=*/1); centerLayout->addWidget(groupKeysGroupBox, /*stretch=*/1); mainLayout->addLayout(centerLayout); mainLayout->addWidget(new KSeparator(Qt::Horizontal, q)); ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, q); QPushButton *saveButton = ui.buttonBox->button(QDialogButtonBox::Save); KGuiItem::assign(saveButton, KStandardGuiItem::save()); KGuiItem::assign(ui.buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); saveButton->setEnabled(false); mainLayout->addWidget(ui.buttonBox); // prevent accidental closing of dialog when pressing Enter while a search field has focus Kleo::unsetAutoDefaultButtons(q); connect(ui.groupNameEdit, &QLineEdit::textChanged, q, [saveButton](const QString &text) { saveButton->setEnabled(!text.trimmed().isEmpty()); }); connect(ui.availableKeysFilter, &QLineEdit::textChanged, ui.availableKeysList, &KeyTreeView::setStringFilter); connect(ui.availableKeysList->view()->selectionModel(), &QItemSelectionModel::selectionChanged, q, [addButton](const QItemSelection &selected, const QItemSelection &) { addButton->setEnabled(!selected.isEmpty()); }); connect(ui.availableKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) { showKeyDetails(index); }); connect(ui.groupKeysFilter, &QLineEdit::textChanged, ui.groupKeysList, &KeyTreeView::setStringFilter); connect(ui.groupKeysList->view()->selectionModel(), &QItemSelectionModel::selectionChanged, q, [removeButton](const QItemSelection &selected, const QItemSelection &) { removeButton->setEnabled(!selected.isEmpty()); }); connect(ui.groupKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) { showKeyDetails(index); }); connect(addButton, &QPushButton::clicked, q, [this]() { addKeysToGroup(); }); connect(removeButton, &QPushButton::clicked, q, [this]() { removeKeysFromGroup(); }); connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &EditGroupDialog::accept); connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &EditGroupDialog::reject); connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, q, [this] { updateFromKeyCache(); }); // calculate default size with enough space for the key list const auto fm = q->fontMetrics(); const QSize sizeHint = q->sizeHint(); const QSize defaultSize = QSize(qMax(sizeHint.width(), 150 * fm.horizontalAdvance(QLatin1Char('x'))), sizeHint.height()); restoreLayout(defaultSize); } ~Private() { saveLayout(); } private: void saveLayout() { KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("EditGroupDialog")); configGroup.writeEntry("Size", q->size()); configGroup.sync(); } void restoreLayout(const QSize &defaultSize) { const KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("EditGroupDialog")); const KConfigGroup availableKeysConfig = configGroup.group(QStringLiteral("AvailableKeysView")); ui.availableKeysList->restoreLayout(availableKeysConfig); const KConfigGroup groupKeysConfig = configGroup.group(QStringLiteral("GroupKeysView")); ui.groupKeysList->restoreLayout(groupKeysConfig); const QSize size = configGroup.readEntry("Size", defaultSize); if (size.isValid()) { q->resize(size); } } void showKeyDetails(const QModelIndex &index) { if (!index.isValid()) { return; } const auto key = index.model()->data(index, KeyList::KeyRole).value(); if (!key.isNull()) { auto cmd = new DetailsCommand(key); cmd->setParentWidget(q); cmd->start(); } } void addKeysToGroup(); void removeKeysFromGroup(); void updateFromKeyCache(); }; void EditGroupDialog::Private::addKeysToGroup() { const std::vector selectedGroupKeys = ui.groupKeysList->selectedKeys(); const std::vector selectedKeys = ui.availableKeysList->selectedKeys(); groupKeysModel->addKeys(selectedKeys); for (const Key &key : selectedKeys) { availableKeysModel->removeKey(key); } ui.groupKeysList->selectKeys(selectedGroupKeys); } void EditGroupDialog::Private::removeKeysFromGroup() { const auto selectedOtherKeys = ui.availableKeysList->selectedKeys(); const std::vector selectedKeys = ui.groupKeysList->selectedKeys(); for (const Key &key : selectedKeys) { groupKeysModel->removeKey(key); } availableKeysModel->addKeys(selectedKeys); ui.availableKeysList->selectKeys(selectedOtherKeys); } void EditGroupDialog::Private::updateFromKeyCache() { const auto selectedGroupKeys = ui.groupKeysList->selectedKeys(); const auto selectedOtherKeys = ui.availableKeysList->selectedKeys(); const auto oldGroupKeys = q->groupKeys(); const auto wasGroupKey = [oldGroupKeys](const Key &key) { - return Kleo::any_of(oldGroupKeys, [key](const auto &k) { + return std::ranges::any_of(oldGroupKeys, [key](const auto &k) { return _detail::ByFingerprint()(k, key); }); }; const auto allKeys = KeyCache::instance()->keys(); std::vector groupKeys; groupKeys.reserve(allKeys.size()); std::vector otherKeys; otherKeys.reserve(otherKeys.size()); std::partition_copy(allKeys.begin(), allKeys.end(), std::back_inserter(groupKeys), std::back_inserter(otherKeys), wasGroupKey); groupKeysModel->setKeys(groupKeys); availableKeysModel->setKeys(otherKeys); ui.groupKeysList->selectKeys(selectedGroupKeys); ui.availableKeysList->selectKeys(selectedOtherKeys); } EditGroupDialog::EditGroupDialog(QWidget *parent) : QDialog(parent) , d(new Private(this)) { setWindowTitle(i18nc("@title:window", "Edit Group")); } EditGroupDialog::~EditGroupDialog() = default; void EditGroupDialog::setInitialFocus(FocusWidget widget) { switch (widget) { case GroupName: d->ui.groupNameEdit->setFocus(); break; case KeysFilter: d->ui.availableKeysFilter->setFocus(); break; default: qCDebug(KLEOPATRA_LOG) << "EditGroupDialog::setInitialFocus - invalid focus widget:" << widget; } } void EditGroupDialog::setGroupName(const QString &name) { d->ui.groupNameEdit->setText(name); } QString EditGroupDialog::groupName() const { return d->ui.groupNameEdit->text().trimmed(); } void EditGroupDialog::setGroupKeys(const std::vector &groupKeys) { d->groupKeysModel->setKeys(groupKeys); // update the keys in the "available keys" list const auto isGroupKey = [groupKeys](const Key &key) { - return Kleo::any_of(groupKeys, [key](const auto &k) { + return std::ranges::any_of(groupKeys, [key](const auto &k) { return _detail::ByFingerprint()(k, key); }); }; auto otherKeys = KeyCache::instance()->keys(); Kleo::erase_if(otherKeys, isGroupKey); d->availableKeysModel->setKeys(otherKeys); } std::vector EditGroupDialog::groupKeys() const { std::vector keys; keys.reserve(d->groupKeysModel->rowCount()); for (int row = 0; row < d->groupKeysModel->rowCount(); ++row) { const QModelIndex index = d->groupKeysModel->index(row, 0); keys.push_back(d->groupKeysModel->key(index)); } return keys; } void EditGroupDialog::showEvent(QShowEvent *event) { QDialog::showEvent(event); // prevent accidental closing of dialog when pressing Enter while a search field has focus Kleo::unsetDefaultButtons(d->ui.buttonBox); } #include "editgroupdialog.moc" #include "moc_editgroupdialog.cpp" diff --git a/src/smartcard/utils.cpp b/src/smartcard/utils.cpp index 9206a56b2..ff705c5df 100644 --- a/src/smartcard/utils.cpp +++ b/src/smartcard/utils.cpp @@ -1,68 +1,68 @@ /* smartcard/utils.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "utils.h" #include "algorithminfo.h" #include "netkeycard.h" #include "openpgpcard.h" #include "pivcard.h" #include #include #include #include #include #include using namespace Kleo::SmartCard; QString Kleo::SmartCard::displayAppName(const std::string &appName) { if (appName == NetKeyCard::AppName) { return i18nc("proper name of a type of smartcard", "NetKey"); } else if (appName == OpenPGPCard::AppName) { return i18nc("proper name of a type of smartcard", "OpenPGP"); } else if (appName == PIVCard::AppName) { return i18nc("proper name of a type of smartcard", "PIV"); } else { return QString::fromStdString(appName); } } std::vector Kleo::SmartCard::getAllowedAlgorithms(const std::vector &supportedAlgorithms) { std::vector result; result.reserve(supportedAlgorithms.size()); Kleo::copy_if(supportedAlgorithms, std::back_inserter(result), [](const auto &algoInfo) { return DeVSCompliance::algorithmIsCompliant(algoInfo.id); }); return result; } std::string Kleo::SmartCard::getPreferredAlgorithm(const std::vector &allowedAlgorithms) { const auto isAllowedAlgorithm = [&allowedAlgorithms](const std::string &algoId) { - return Kleo::any_of(allowedAlgorithms, [&algoId](const auto &algoInfo) { + return std::ranges::any_of(allowedAlgorithms, [&algoId](const auto &algoInfo) { return algoInfo.id == algoId; }); }; const auto &preferredAlgos = Kleo::preferredAlgorithms(); const auto defaultAlgoIt = Kleo::find_if(preferredAlgos, isAllowedAlgorithm); if (defaultAlgoIt != preferredAlgos.end()) { return *defaultAlgoIt; } else { qCWarning(KLEOPATRA_LOG) << __func__ << "- No preferred algorithm is allowed. Using first allowed algorithm as default."; return !allowedAlgorithms.empty() ? allowedAlgorithms.front().id : std::string{}; } } diff --git a/src/utils/clipboardmenu.cpp b/src/utils/clipboardmenu.cpp index 33a02fcee..a98334766 100644 --- a/src/utils/clipboardmenu.cpp +++ b/src/utils/clipboardmenu.cpp @@ -1,149 +1,149 @@ /* SPDX-FileCopyrightText: 2014-2021 Laurent Montel SPDX-License-Identifier: GPL-2.0-only */ #include #include "clipboardmenu.h" #include "kdtoolsglobal.h" #include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Commands; ClipboardMenu::ClipboardMenu(QObject *parent) : QObject{parent} { mClipboardMenu = new KActionMenu(i18n("Clipboard"), this); mImportClipboardAction = new QAction(i18n("Certificate Import"), this); mEncryptClipboardAction = new QAction(i18n("Encrypt..."), this); const Kleo::Settings settings{}; if (settings.cmsEnabled() && settings.cmsSigningAllowed()) { mSmimeSignClipboardAction = new QAction(i18n("S/MIME-Sign..."), this); } mOpenPGPSignClipboardAction = new QAction(i18n("OpenPGP-Sign..."), this); mDecryptVerifyClipboardAction = new QAction(i18n("Decrypt/Verify..."), this); KDAB_SET_OBJECT_NAME(mClipboardMenu); KDAB_SET_OBJECT_NAME(mImportClipboardAction); KDAB_SET_OBJECT_NAME(mEncryptClipboardAction); KDAB_SET_OBJECT_NAME(mSmimeSignClipboardAction); KDAB_SET_OBJECT_NAME(mOpenPGPSignClipboardAction); KDAB_SET_OBJECT_NAME(mDecryptVerifyClipboardAction); connect(mImportClipboardAction, &QAction::triggered, this, &ClipboardMenu::slotImportClipboard); connect(mEncryptClipboardAction, &QAction::triggered, this, &ClipboardMenu::slotEncryptClipboard); if (mSmimeSignClipboardAction) { connect(mSmimeSignClipboardAction, &QAction::triggered, this, &ClipboardMenu::slotSMIMESignClipboard); } connect(mOpenPGPSignClipboardAction, &QAction::triggered, this, &ClipboardMenu::slotOpenPGPSignClipboard); connect(mDecryptVerifyClipboardAction, &QAction::triggered, this, &ClipboardMenu::slotDecryptVerifyClipboard); mClipboardMenu->addAction(mImportClipboardAction); mClipboardMenu->addAction(mEncryptClipboardAction); if (mSmimeSignClipboardAction) { mClipboardMenu->addAction(mSmimeSignClipboardAction); } mClipboardMenu->addAction(mOpenPGPSignClipboardAction); mClipboardMenu->addAction(mDecryptVerifyClipboardAction); connect(QApplication::clipboard(), &QClipboard::changed, this, &ClipboardMenu::slotEnableDisableActions); connect(KeyCache::instance().get(), &KeyCache::keyListingDone, this, &ClipboardMenu::slotEnableDisableActions); slotEnableDisableActions(); } ClipboardMenu::~ClipboardMenu() = default; void ClipboardMenu::setMainWindow(MainWindow *window) { mWindow = window; } KActionMenu *ClipboardMenu::clipboardMenu() const { return mClipboardMenu; } void ClipboardMenu::startCommand(Command *cmd) { Q_ASSERT(cmd); cmd->setParent(mWindow); cmd->start(); } void ClipboardMenu::slotImportClipboard() { startCommand(new ImportCertificateFromClipboardCommand(nullptr)); } void ClipboardMenu::slotEncryptClipboard() { startCommand(new EncryptClipboardCommand(nullptr)); } void ClipboardMenu::slotOpenPGPSignClipboard() { startCommand(new SignClipboardCommand(GpgME::OpenPGP, nullptr)); } void ClipboardMenu::slotSMIMESignClipboard() { startCommand(new SignClipboardCommand(GpgME::CMS, nullptr)); } void ClipboardMenu::slotDecryptVerifyClipboard() { startCommand(new DecryptVerifyClipboardCommand(nullptr)); } namespace { bool hasSigningKeys(GpgME::Protocol protocol) { if (!KeyCache::instance()->initialized()) { return false; } - return Kleo::any_of(KeyCache::instance()->keys(), [protocol](const auto &k) { + return std::ranges::any_of(KeyCache::instance()->keys(), [protocol](const auto &k) { return k.hasSecret() && Kleo::keyHasSign(k) && (k.protocol() == protocol); }); } } void ClipboardMenu::slotEnableDisableActions() { const QSignalBlocker blocker(QApplication::clipboard()); mImportClipboardAction->setEnabled(ImportCertificateFromClipboardCommand::canImportCurrentClipboard()); mEncryptClipboardAction->setEnabled(EncryptClipboardCommand::canEncryptCurrentClipboard()); mOpenPGPSignClipboardAction->setEnabled(SignClipboardCommand::canSignCurrentClipboard() && hasSigningKeys(GpgME::OpenPGP)); if (mSmimeSignClipboardAction) { mSmimeSignClipboardAction->setEnabled(SignClipboardCommand::canSignCurrentClipboard() && hasSigningKeys(GpgME::CMS)); } mDecryptVerifyClipboardAction->setEnabled(DecryptVerifyClipboardCommand::canDecryptVerifyCurrentClipboard()); } #include "moc_clipboardmenu.cpp" diff --git a/src/view/keylistcontroller.cpp b/src/view/keylistcontroller.cpp index 772717566..8f38d181a 100644 --- a/src/view/keylistcontroller.cpp +++ b/src/view/keylistcontroller.cpp @@ -1,1019 +1,1021 @@ /* -*- mode: c++; c-basic-offset:4 -*- view/keylistcontroller.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2022 Felix Tiede SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keylistcontroller.h" #include "tabwidget.h" #include #include #include "commands/exportcertificatecommand.h" #include "commands/exportopenpgpcertstoservercommand.h" #include "kleopatra_debug.h" #include "tooltippreferences.h" #include #ifdef MAILAKONADI_ENABLED #include "commands/exportopenpgpcerttoprovidercommand.h" #endif // MAILAKONADI_ENABLED #include "commands/adduseridcommand.h" #include "commands/certifycertificatecommand.h" #include "commands/changeexpirycommand.h" #include "commands/changeownertrustcommand.h" #include "commands/changepassphrasecommand.h" #include "commands/changeroottrustcommand.h" #include "commands/checksumcreatefilescommand.h" #include "commands/checksumverifyfilescommand.h" #include "commands/clearcrlcachecommand.h" #include "commands/creategroupcommand.h" #include "commands/decryptverifyfilescommand.h" #include "commands/deletecertificatescommand.h" #include "commands/detailscommand.h" #include "commands/dumpcertificatecommand.h" #include "commands/dumpcrlcachecommand.h" #include "commands/exportpaperkeycommand.h" #include "commands/exportsecretkeycommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/importcrlcommand.h" #include "commands/lookupcertificatescommand.h" #include "commands/newcertificatesigningrequestcommand.h" #include "commands/newopenpgpcertificatecommand.h" #include "commands/refreshopenpgpcertscommand.h" #include "commands/refreshx509certscommand.h" #include "commands/reloadkeyscommand.h" #include "commands/revokecertificationcommand.h" #include "commands/revokekeycommand.h" #include "commands/signencryptfilescommand.h" #include "commands/signencryptfoldercommand.h" #include #include #include #include #include #include #include #include #include #include #include #include #include -// needed for GPGME_VERSION_NUMBER -#include - using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; +namespace ranges = std::ranges; + class KeyListController::Private { friend class ::Kleo::KeyListController; KeyListController *const q; public: explicit Private(KeyListController *qq); ~Private(); void connectView(QAbstractItemView *view); void connectCommand(Command *cmd); void connectTabWidget(); void disconnectTabWidget(); void addCommand(Command *cmd) { connectCommand(cmd); - commands.insert(std::lower_bound(commands.begin(), commands.end(), cmd), cmd); + commands.insert(ranges::lower_bound(commands, cmd), cmd); } void addView(QAbstractItemView *view) { connectView(view); - views.insert(std::lower_bound(views.begin(), views.end(), view), view); + views.insert(ranges::lower_bound(views, view), view); } void removeView(QAbstractItemView *view) { view->disconnect(q); view->selectionModel()->disconnect(q); - views.erase(std::remove(views.begin(), views.end(), view), views.end()); + std::erase(views, view); } public: void slotDestroyed(QObject *o) { qCDebug(KLEOPATRA_LOG) << (void *)o; - views.erase(std::remove(views.begin(), views.end(), o), views.end()); - commands.erase(std::remove(commands.begin(), commands.end(), o), commands.end()); + std::erase(views, o); + std::erase(commands, o); } void slotDoubleClicked(const QModelIndex &idx); void slotActivated(const QModelIndex &idx); void slotSelectionChanged(const QItemSelection &old, const QItemSelection &new_); void slotContextMenu(const QPoint &pos); void slotCommandFinished(); void slotActionTriggered(QAction *action); void slotCurrentViewChanged(QAbstractItemView *view) { - if (view && !std::binary_search(views.cbegin(), views.cend(), view)) { + if (view && !ranges::binary_search(views, view)) { qCDebug(KLEOPATRA_LOG) << "you need to register view" << view << "before trying to set it as the current view!"; addView(view); } currentView = view; q->enableDisableActions(view ? view->selectionModel() : nullptr); } private: int toolTipOptions() const; private: static Command::Restrictions calculateRestrictionsMask(const QItemSelectionModel *sm); private: struct action_item { QPointer action; Command::Restrictions restrictions; Command *(*createCommand)(QAbstractItemView *, KeyListController *); }; std::vector actions; std::vector views; std::vector commands; QPointer parentWidget; QPointer tabWidget; QPointer currentView; QPointer flatModel, hierarchicalModel; std::vector m_connections; }; KeyListController::Private::Private(KeyListController *qq) : q(qq) , actions() , views() , commands() , parentWidget() , tabWidget() , flatModel() , hierarchicalModel() { } KeyListController::Private::~Private() { } KeyListController::KeyListController(QObject *p) : QObject(p) , d(new Private(this)) { } KeyListController::~KeyListController() { } void KeyListController::addView(QAbstractItemView *view) { - if (!view || std::binary_search(d->views.cbegin(), d->views.cend(), view)) { + if (!view || ranges::binary_search(d->views, view)) { return; } d->addView(view); } void KeyListController::removeView(QAbstractItemView *view) { - if (!view || !std::binary_search(d->views.cbegin(), d->views.cend(), view)) { + if (!view || !ranges::binary_search(d->views, view)) { return; } d->removeView(view); } void KeyListController::setCurrentView(QAbstractItemView *view) { d->slotCurrentViewChanged(view); } std::vector KeyListController::views() const { return d->views; } void KeyListController::setFlatModel(AbstractKeyListModel *model) { if (model == d->flatModel) { return; } d->flatModel = model; if (model) { model->setToolTipOptions(d->toolTipOptions()); } } void KeyListController::setHierarchicalModel(AbstractKeyListModel *model) { if (model == d->hierarchicalModel) { return; } d->hierarchicalModel = model; if (model) { model->setToolTipOptions(d->toolTipOptions()); } } void KeyListController::setTabWidget(TabWidget *tabWidget) { if (tabWidget == d->tabWidget) { return; } d->disconnectTabWidget(); d->tabWidget = tabWidget; d->connectTabWidget(); d->slotCurrentViewChanged(tabWidget ? tabWidget->currentView() : nullptr); } void KeyListController::setParentWidget(QWidget *parent) { d->parentWidget = parent; } QWidget *KeyListController::parentWidget() const { return d->parentWidget; } void KeyListController::Private::connectTabWidget() { if (!tabWidget) { return; } const auto views = tabWidget->views(); - std::for_each(views.cbegin(), views.cend(), [this](QAbstractItemView *view) { + ranges::for_each(views, [this](QAbstractItemView *view) { addView(view); }); m_connections.reserve(3); m_connections.push_back(connect(tabWidget, &TabWidget::viewAdded, q, &KeyListController::addView)); m_connections.push_back(connect(tabWidget, &TabWidget::viewAboutToBeRemoved, q, &KeyListController::removeView)); m_connections.push_back(connect(tabWidget, &TabWidget::currentViewChanged, q, [this](QAbstractItemView *view) { slotCurrentViewChanged(view); })); } void KeyListController::Private::disconnectTabWidget() { if (!tabWidget) { return; } for (const auto &connection : m_connections) { disconnect(connection); } m_connections.clear(); const auto views = tabWidget->views(); - std::for_each(views.cbegin(), views.cend(), [this](QAbstractItemView *view) { + ranges::for_each(views, [this](QAbstractItemView *view) { removeView(view); }); } AbstractKeyListModel *KeyListController::flatModel() const { return d->flatModel; } AbstractKeyListModel *KeyListController::hierarchicalModel() const { return d->hierarchicalModel; } QAbstractItemView *KeyListController::currentView() const { return d->currentView; } TabWidget *KeyListController::tabWidget() const { return d->tabWidget; } void KeyListController::createActions(KActionCollection *coll) { const std::vector common_and_openpgp_action_data = { // File menu { "file_new_certificate", i18n("New OpenPGP Key Pair..."), i18n("Create a new OpenPGP certificate"), "view-certificate-add", nullptr, nullptr, QStringLiteral("Ctrl+N"), }, { "file_export_certificates", i18n("Export..."), i18n("Export the selected certificate (public key) to a file"), "view-certificate-export", nullptr, nullptr, QStringLiteral("Ctrl+E"), }, { "file_export_certificates_to_server", i18n("Publish on Server..."), i18n("Publish the selected certificate (public key) on a public keyserver"), "view-certificate-export-server", nullptr, nullptr, QStringLiteral("Ctrl+Shift+E"), }, #ifdef MAILAKONADI_ENABLED { "file_export_certificate_to_provider", i18n("Publish at Mail Provider..."), i18n("Publish the selected certificate (public key) at mail provider's Web Key Directory if offered"), "view-certificate-export", nullptr, nullptr, QString(), }, #endif // MAILAKONADI_ENABLED { "file_export_secret_keys", i18n("Backup Secret Keys..."), QString(), "view-certificate-export-secret", nullptr, nullptr, QString(), }, { "file_export_paper_key", i18n("Print Secret Key..."), QString(), "document-print", nullptr, nullptr, QString(), }, { "file_lookup_certificates", i18n("Lookup on Server..."), i18n("Search for certificates online using a public keyserver"), "edit-find", nullptr, nullptr, QStringLiteral("Shift+Ctrl+I"), }, { "file_import_certificates", i18n("Import..."), i18n("Import a certificate from a file"), "view-certificate-import", nullptr, nullptr, QStringLiteral("Ctrl+I"), }, { "file_decrypt_verify_files", i18n("Decrypt/Verify..."), i18n("Decrypt and/or verify files"), "document-edit-decrypt-verify", nullptr, nullptr, QString(), }, { "file_sign_encrypt_files", i18n("Sign/Encrypt..."), i18n("Encrypt and/or sign files"), "document-edit-sign-encrypt", nullptr, nullptr, QString(), }, { "file_sign_encrypt_folder", i18n("Sign/Encrypt Folder..."), i18n("Encrypt and/or sign folders"), "folder-edit-sign-encrypt-symbolic", nullptr, nullptr, QString(), }, { "file_checksum_create_files", i18n("Create Checksum Files..."), QString(), nullptr /*"document-checksum-create"*/, nullptr, nullptr, QString(), }, { "file_checksum_verify_files", i18n("Verify Checksum Files..."), QString(), nullptr /*"document-checksum-verify"*/, nullptr, nullptr, QString(), }, // View menu { "view_redisplay", i18n("Redisplay"), QString(), "view-refresh", nullptr, nullptr, QStringLiteral("F5"), }, { "view_stop_operations", i18n("Stop Operation"), QString(), "process-stop", this, [this](bool) { cancelCommands(); }, QStringLiteral("Escape"), RegularQAction, Disabled, }, { "view_certificate_details", i18n("Details"), QString(), "dialog-information", nullptr, nullptr, QString(), }, // Certificate menu { "certificates_revoke", i18n("Revoke Certificate..."), i18n("Revoke the selected OpenPGP certificate"), "view-certificate-revoke", nullptr, nullptr, {}, }, { "certificates_delete", i18n("Delete"), i18n("Delete selected certificates"), "edit-delete", nullptr, nullptr, QStringLiteral("Delete"), }, { "certificates_certify_certificate", i18n("Certify..."), i18n("Certify the validity of the selected certificate"), "view-certificate-sign", nullptr, nullptr, QString(), }, { "certificates_revoke_certification", i18n("Revoke Certification..."), i18n("Revoke the certification of the selected certificate"), "view-certificate-revoke", nullptr, nullptr, QString(), }, { "certificates_change_expiry", i18n("Change End of Validity Period..."), QString(), nullptr, nullptr, nullptr, QString(), }, { "certificates_change_owner_trust", i18nc("@action:inmenu", "Change Certification Power..."), i18nc("@info:tooltip", "Grant or revoke the certification power of the selected certificate"), nullptr, nullptr, nullptr, QString(), }, { "certificates_change_passphrase", i18n("Change Passphrase..."), QString(), nullptr, nullptr, nullptr, QString(), }, { "certificates_add_userid", i18n("Add User ID..."), QString(), nullptr, nullptr, nullptr, QString(), }, { "certificates_create_group", i18nc("@action:inmenu", "Create Group..."), i18nc("@info:tooltip", "Create a group from the selected certificates"), "resource-group-new", nullptr, nullptr, QString(), }, // Tools menu { "tools_refresh_openpgp_certificates", i18n("Refresh OpenPGP Certificates"), QString(), "view-refresh", nullptr, nullptr, QString(), }, // Window menu // (come from TabWidget) // Help menu // (come from MainWindow) }; static const action_data cms_create_csr_action_data = { "file_new_certificate_signing_request", i18n("New S/MIME Certification Request..."), i18n("Create a new S/MIME certificate signing request (CSR)"), "view-certificate-add", nullptr, nullptr, {}, }; static const std::vector cms_action_data = { // Certificate menu { "certificates_trust_root", i18n("Trust Root Certificate"), QString(), nullptr, nullptr, nullptr, QString(), }, { "certificates_distrust_root", i18n("Distrust Root Certificate"), QString(), nullptr, nullptr, nullptr, QString(), }, { "certificates_dump_certificate", i18n("Technical Details"), QString(), nullptr, nullptr, nullptr, QString(), }, // Tools menu { "tools_refresh_x509_certificates", i18n("Refresh S/MIME Certificates"), QString(), "view-refresh", nullptr, nullptr, QString(), }, { "crl_clear_crl_cache", i18n("Clear CRL Cache"), QString(), nullptr, nullptr, nullptr, QString(), }, { "crl_dump_crl_cache", i18n("Dump CRL Cache"), QString(), nullptr, nullptr, nullptr, QString(), }, { "crl_import_crl", i18n("Import CRL From File..."), QString(), nullptr, nullptr, nullptr, QString(), }, }; std::vector action_data = common_and_openpgp_action_data; if (const Kleo::Settings settings{}; settings.cmsEnabled()) { if (settings.cmsCertificateCreationAllowed()) { action_data.push_back(cms_create_csr_action_data); } action_data.reserve(action_data.size() + cms_action_data.size()); - std::copy(std::begin(cms_action_data), std::end(cms_action_data), std::back_inserter(action_data)); + ranges::copy(cms_action_data, std::back_inserter(action_data)); } make_actions_from_data(action_data, coll); if (QAction *action = coll->action(QStringLiteral("view_stop_operations"))) { connect(this, &KeyListController::commandsExecuting, action, &QAction::setEnabled); } // ### somehow make this better... registerActionForCommand(coll->action(QStringLiteral("file_new_certificate"))); registerActionForCommand(coll->action(QStringLiteral("file_new_certificate_signing_request"))); //--- registerActionForCommand(coll->action(QStringLiteral("file_lookup_certificates"))); registerActionForCommand(coll->action(QStringLiteral("file_import_certificates"))); //--- registerActionForCommand(coll->action(QStringLiteral("file_export_certificates"))); registerActionForCommand(coll->action(QStringLiteral("file_export_secret_keys"))); registerActionForCommand(coll->action(QStringLiteral("file_export_paper_key"))); registerActionForCommand(coll->action(QStringLiteral("file_export_certificates_to_server"))); #ifdef MAILAKONADI_ENABLED registerActionForCommand(coll->action(QStringLiteral("file_export_certificate_to_provider"))); #endif // MAILAKONADI_ENABLED //--- registerActionForCommand(coll->action(QStringLiteral("file_decrypt_verify_files"))); registerActionForCommand(coll->action(QStringLiteral("file_sign_encrypt_files"))); registerActionForCommand(coll->action(QStringLiteral("file_sign_encrypt_folder"))); //--- registerActionForCommand(coll->action(QStringLiteral("file_checksum_create_files"))); registerActionForCommand(coll->action(QStringLiteral("file_checksum_verify_files"))); registerActionForCommand(coll->action(QStringLiteral("view_redisplay"))); // coll->action( "view_stop_operations" ) <-- already dealt with in make_actions_from_data() registerActionForCommand(coll->action(QStringLiteral("view_certificate_details"))); registerActionForCommand(coll->action(QStringLiteral("certificates_change_owner_trust"))); registerActionForCommand(coll->action(QStringLiteral("certificates_trust_root"))); registerActionForCommand(coll->action(QStringLiteral("certificates_distrust_root"))); //--- registerActionForCommand(coll->action(QStringLiteral("certificates_certify_certificate"))); if (RevokeCertificationCommand::isSupported()) { registerActionForCommand(coll->action(QStringLiteral("certificates_revoke_certification"))); } //--- registerActionForCommand(coll->action(QStringLiteral("certificates_change_expiry"))); registerActionForCommand(coll->action(QStringLiteral("certificates_change_passphrase"))); registerActionForCommand(coll->action(QStringLiteral("certificates_add_userid"))); registerActionForCommand(coll->action(QStringLiteral("certificates_create_group"))); //--- registerActionForCommand(coll->action(QStringLiteral("certificates_revoke"))); registerActionForCommand(coll->action(QStringLiteral("certificates_delete"))); //--- registerActionForCommand(coll->action(QStringLiteral("certificates_dump_certificate"))); registerActionForCommand(coll->action(QStringLiteral("tools_refresh_x509_certificates"))); registerActionForCommand(coll->action(QStringLiteral("tools_refresh_openpgp_certificates"))); //--- registerActionForCommand(coll->action(QStringLiteral("crl_import_crl"))); //--- registerActionForCommand(coll->action(QStringLiteral("crl_clear_crl_cache"))); registerActionForCommand(coll->action(QStringLiteral("crl_dump_crl_cache"))); enableDisableActions(nullptr); } void KeyListController::registerAction(QAction *action, Command::Restrictions restrictions, Command *(*create)(QAbstractItemView *, KeyListController *)) { if (!action) { return; } Q_ASSERT(!action->isCheckable()); // can be added later, for now, disallow const Private::action_item ai = {action, restrictions, create}; connect(action, &QAction::triggered, this, [this, action]() { d->slotActionTriggered(action); }); d->actions.push_back(ai); } void KeyListController::registerCommand(Command *cmd) { - if (!cmd || std::binary_search(d->commands.cbegin(), d->commands.cend(), cmd)) { + if (!cmd || ranges::binary_search(d->commands, cmd)) { return; } d->addCommand(cmd); qCDebug(KLEOPATRA_LOG) << (void *)cmd; if (d->commands.size() == 1) { Q_EMIT commandsExecuting(true); } } bool KeyListController::hasRunningCommands() const { return !d->commands.empty(); } bool KeyListController::shutdownWarningRequired() const { - return std::any_of(d->commands.cbegin(), d->commands.cend(), std::mem_fn(&Command::warnWhenRunningAtShutdown)); + return ranges::any_of(d->commands, std::mem_fn(&Command::warnWhenRunningAtShutdown)); } // slot void KeyListController::cancelCommands() { - std::for_each(d->commands.begin(), d->commands.end(), std::mem_fn(&Command::cancel)); + ranges::for_each(d->commands, std::mem_fn(&Command::cancel)); } void KeyListController::Private::connectView(QAbstractItemView *view) { connect(view, &QObject::destroyed, q, [this](QObject *obj) { slotDestroyed(obj); }); connect(view, &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) { slotDoubleClicked(index); }); connect(view, &QAbstractItemView::activated, q, [this](const QModelIndex &index) { slotActivated(index); }); connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this](const QItemSelection &oldSel, const QItemSelection &newSel) { slotSelectionChanged(oldSel, newSel); }); view->setContextMenuPolicy(Qt::CustomContextMenu); connect(view, &QWidget::customContextMenuRequested, q, [this](const QPoint &pos) { slotContextMenu(pos); }); } void KeyListController::Private::connectCommand(Command *cmd) { if (!cmd) { return; } connect(cmd, &QObject::destroyed, q, [this](QObject *obj) { slotDestroyed(obj); }); connect(cmd, &Command::finished, q, [this] { slotCommandFinished(); }); // connect( cmd, SIGNAL(canceled()), q, SLOT(slotCommandCanceled()) ); connect(cmd, &Command::progress, q, &KeyListController::progress); } void KeyListController::Private::slotDoubleClicked(const QModelIndex &idx) { QAbstractItemView *const view = qobject_cast(q->sender()); - if (!view || !std::binary_search(views.cbegin(), views.cend(), view)) { + if (!view || !ranges::binary_search(views, view)) { return; } if (const auto *const keyListModel = dynamic_cast(view->model())) { DetailsCommand *const c = new DetailsCommand{keyListModel->key(idx)}; c->setParentWidget(parentWidget ? parentWidget : view); c->start(); } } void KeyListController::Private::slotActivated(const QModelIndex &idx) { Q_UNUSED(idx) QAbstractItemView *const view = qobject_cast(q->sender()); - if (!view || !std::binary_search(views.cbegin(), views.cend(), view)) { + if (!view || !ranges::binary_search(views, view)) { return; } } void KeyListController::Private::slotSelectionChanged(const QItemSelection &old, const QItemSelection &new_) { Q_UNUSED(old) Q_UNUSED(new_) const QItemSelectionModel *const sm = qobject_cast(q->sender()); if (!sm) { return; } q->enableDisableActions(sm); } void KeyListController::Private::slotContextMenu(const QPoint &p) { QAbstractItemView *const view = qobject_cast(q->sender()); - if (view && std::binary_search(views.cbegin(), views.cend(), view)) { + if (view && ranges::binary_search(views, view)) { Q_EMIT q->contextMenuRequested(view, view->viewport()->mapToGlobal(p)); } else { qCDebug(KLEOPATRA_LOG) << "sender is not a QAbstractItemView*!"; } } void KeyListController::Private::slotCommandFinished() { Command *const cmd = qobject_cast(q->sender()); - if (!cmd || !std::binary_search(commands.cbegin(), commands.cend(), cmd)) { + if (!cmd || !ranges::binary_search(commands, cmd)) { return; } qCDebug(KLEOPATRA_LOG) << (void *)cmd; if (commands.size() == 1) { Q_EMIT q->commandsExecuting(false); } } void KeyListController::enableDisableActions(const QItemSelectionModel *sm) const { const Command::Restrictions restrictionsMask = d->calculateRestrictionsMask(sm); for (const Private::action_item &ai : std::as_const(d->actions)) if (ai.action) { ai.action->setEnabled(ai.restrictions == (ai.restrictions & restrictionsMask)); } } static bool all_secret_are_not_owner_trust_ultimate(const std::vector &keys) { for (const Key &key : keys) if (key.hasSecret() && key.ownerTrust() == Key::Ultimate) { return false; } return true; } Command::Restrictions find_root_restrictions(const std::vector &keys) { bool trusted = false, untrusted = false; for (const Key &key : keys) if (key.isRoot()) if (key.userID(0).validity() == UserID::Ultimate) { trusted = true; } else { untrusted = true; } else { return Command::NoRestriction; } if (trusted) if (untrusted) { return Command::NoRestriction; } else { return Command::MustBeTrustedRoot; } else if (untrusted) { return Command::MustBeUntrustedRoot; } else { return Command::NoRestriction; } } +static bool secretSubkeyDataAvailable(const Subkey &subkey) +{ + return subkey.isSecret() && !subkey.isCardKey(); +}; + Command::Restrictions KeyListController::Private::calculateRestrictionsMask(const QItemSelectionModel *sm) { if (!sm) { return Command::NoRestriction; } const KeyListModelInterface *const m = dynamic_cast(sm->model()); if (!m) { return Command::NoRestriction; } const std::vector keys = m->keys(sm->selectedRows()); if (keys.empty()) { return Command::NoRestriction; } Command::Restrictions result = Command::NeedSelection; if (keys.size() == 1) { result |= Command::OnlyOneKey; } -#if GPGME_VERSION_NUMBER >= 0x011102 // 1.17.2 // we need to check the primary subkey because Key::hasSecret() is also true if just the secret key stub of an offline key is available const auto primaryKeyCanBeUsedForSecretKeyOperations = [](const auto &k) { return k.subkey(0).isSecret(); }; -#else - // older versions of GpgME did not always set the secret flag for card keys - const auto primaryKeyCanBeUsedForSecretKeyOperations = [](const auto &k) { - return k.subkey(0).isSecret() || k.subkey(0).isCardKey(); - }; -#endif - if (std::all_of(keys.cbegin(), keys.cend(), primaryKeyCanBeUsedForSecretKeyOperations)) { + if (ranges::all_of(keys, primaryKeyCanBeUsedForSecretKeyOperations)) { result |= Command::NeedSecretKey; } - if (std::all_of(std::begin(keys), std::end(keys), [](const auto &k) { - return k.subkey(0).isSecret() && !k.subkey(0).isCardKey(); + if (ranges::all_of(keys, [](const auto &k) { + return ranges::any_of(k.subkeys(), &secretSubkeyDataAvailable); + })) { + result |= Command::NeedSecretSubkeyData; + } + if ((result & Command::NeedSecretSubkeyData) && ranges::all_of(keys, [](const auto &k) { + return secretSubkeyDataAvailable(k.subkey(0)); })) { - result |= Command::NeedSecretKeyData; + result |= Command::NeedSecretPrimaryKeyData; } - if (std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { + if (ranges::all_of(keys, [](const Key &key) { return key.protocol() == OpenPGP; })) { result |= Command::MustBeOpenPGP; - } else if (std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { + } else if (ranges::all_of(keys, [](const Key &key) { return key.protocol() == CMS; })) { result |= Command::MustBeCMS; } - if (Kleo::all_of(keys, [](const auto &key) { + if (ranges::all_of(keys, [](const auto &key) { return !key.isBad(); })) { result |= Command::MustBeValid; } if (all_secret_are_not_owner_trust_ultimate(keys)) { result |= Command::MayOnlyBeSecretKeyIfOwnerTrustIsNotYetUltimate; } result |= find_root_restrictions(keys); if (const ReaderStatus *rs = ReaderStatus::instance()) { if (!rs->firstCardWithNullPin().empty()) { result |= Command::AnyCardHasNullPin; } } return result; } void KeyListController::Private::slotActionTriggered(QAction *sender) { - const auto it = std::find_if(actions.cbegin(), actions.cend(), [sender](const action_item &item) { + const auto it = ranges::find_if(actions, [sender](const action_item &item) { return item.action == sender; }); if (it != actions.end()) if (Command *const c = it->createCommand(this->currentView, q)) { if (parentWidget) { c->setParentWidget(parentWidget); } c->start(); } else qCDebug(KLEOPATRA_LOG) << "createCommand() == NULL for action(?) \"" << qPrintable(sender->objectName()) << "\""; else { qCDebug(KLEOPATRA_LOG) << "I don't know anything about action(?) \"%s\"", qPrintable(sender->objectName()); } } int KeyListController::Private::toolTipOptions() const { using namespace Kleo::Formatting; static const int validityFlags = Validity | Issuer | ExpiryDates | CertificateUsage; static const int ownerFlags = Subject | UserIDs | OwnerTrust; static const int detailsFlags = StorageLocation | CertificateType | SerialNumber | Fingerprint; const TooltipPreferences prefs; int flags = KeyID; flags |= prefs.showValidity() ? validityFlags : 0; flags |= prefs.showOwnerInformation() ? ownerFlags : 0; flags |= prefs.showCertificateDetails() ? detailsFlags : 0; return flags; } void KeyListController::updateConfig() { const int opts = d->toolTipOptions(); if (d->flatModel) { d->flatModel->setToolTipOptions(opts); } if (d->hierarchicalModel) { d->hierarchicalModel->setToolTipOptions(opts); } } #include "moc_keylistcontroller.cpp" diff --git a/src/view/searchbar.cpp b/src/view/searchbar.cpp index 9f2882e2a..2e2ea49c6 100644 --- a/src/view/searchbar.cpp +++ b/src/view/searchbar.cpp @@ -1,236 +1,236 @@ /* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*- view/searchbar.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "searchbar.h" #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; namespace { class ProxyModel : public QSortFilterProxyModel { Q_OBJECT public: using QSortFilterProxyModel::QSortFilterProxyModel; protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override { const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); const auto matchContexts = qvariant_cast(sourceModel()->data(index, KeyFilterManager::FilterMatchContextsRole)); return matchContexts & KeyFilter::Filtering; } }; } class SearchBar::Private { friend class ::Kleo::SearchBar; SearchBar *const q; public: explicit Private(SearchBar *qq); ~Private(); private: void slotKeyFilterChanged(int idx) { Q_EMIT q->keyFilterChanged(keyFilter(idx)); } std::shared_ptr keyFilter(int idx) const { const QModelIndex mi = proxyModel->mapToSource(proxyModel->index(idx, 0)); return KeyFilterManager::instance()->fromModelIndex(mi); } std::shared_ptr currentKeyFilter() const { return keyFilter(combo->currentIndex()); } QString currentKeyFilterID() const { if (const std::shared_ptr f = currentKeyFilter()) { return f->id(); } else { return QString(); } } static auto notCertifiedKeysFilterId() { static const QString filterId = QStringLiteral("not-certified-certificates"); return filterId; } void listNotCertifiedKeys() const { lineEdit->clear(); combo->setCurrentIndex(combo->findData(notCertifiedKeysFilterId())); Q_EMIT q->keyFilterChanged(keyFilter(combo->currentIndex())); } void showOrHideCertifyButton() const { if (!KeyCache::instance()->initialized()) { return; } const auto filter = KeyFilterManager::instance()->keyFilterByID(notCertifiedKeysFilterId()); if (filter) { - if (Kleo::any_of(KeyCache::instance()->keys(), [filter](const auto &key) { + if (std::ranges::any_of(KeyCache::instance()->keys(), [filter](const auto &key) { return filter->matches(key, KeyFilter::Filtering); })) { certifyButton->show(); return; } } else { qCDebug(KLEOPATRA_LOG) << __func__ << "Key filter with id" << notCertifiedKeysFilterId() << "not found"; } certifyButton->hide(); } private: ProxyModel *proxyModel; QLineEdit *lineEdit; QComboBox *combo; QPushButton *certifyButton; }; SearchBar::Private::Private(SearchBar *qq) : q(qq) { auto layout = new QHBoxLayout(q); layout->setContentsMargins(0, 0, 0, 0); lineEdit = new QLineEdit(q); lineEdit->setClearButtonEnabled(true); lineEdit->setPlaceholderText(i18n("Search...")); lineEdit->setAccessibleName(i18n("Filter certificates by text")); lineEdit->setToolTip(i18n("Show only certificates that match the entered search term.")); layout->addWidget(lineEdit, /*stretch=*/1); combo = new QComboBox(q); combo->setAccessibleName(i18n("Filter certificates by category")); combo->setToolTip(i18n("Show only certificates that belong to the selected category.")); layout->addWidget(combo); certifyButton = new QPushButton(q); certifyButton->setIcon(QIcon::fromTheme(QStringLiteral("security-medium"))); certifyButton->setAccessibleName(i18n("Show not certified certificates")); certifyButton->setToolTip( i18n("Some certificates are not yet certified. " "Click here to see a list of these certificates." "

    " "Certification is required to make sure that the certificates " "actually belong to the identity they claim to belong to.")); certifyButton->hide(); layout->addWidget(certifyButton); proxyModel = new ProxyModel{q}; proxyModel->setSourceModel(KeyFilterManager::instance()->model()); proxyModel->sort(0, Qt::AscendingOrder); combo->setModel(proxyModel); KDAB_SET_OBJECT_NAME(layout); KDAB_SET_OBJECT_NAME(lineEdit); KDAB_SET_OBJECT_NAME(combo); KDAB_SET_OBJECT_NAME(certifyButton); connect(lineEdit, &QLineEdit::textChanged, q, &SearchBar::stringFilterChanged); connect(combo, &QComboBox::currentIndexChanged, q, [this](int index) { slotKeyFilterChanged(index); }); connect(certifyButton, &QPushButton::clicked, q, [this]() { listNotCertifiedKeys(); }); connect(KeyCache::instance().get(), &KeyCache::keyListingDone, q, [this]() { showOrHideCertifyButton(); }); showOrHideCertifyButton(); } SearchBar::Private::~Private() { } SearchBar::SearchBar(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f) , d(new Private(this)) { } SearchBar::~SearchBar() { } void SearchBar::updateClickMessage(const QString &shortcutStr) { d->lineEdit->setPlaceholderText(i18n("Search...<%1>", shortcutStr)); } // slot void SearchBar::setStringFilter(const QString &filter) { d->lineEdit->setText(filter); } // slot void SearchBar::setKeyFilter(const std::shared_ptr &kf) { const QModelIndex sourceIndex = KeyFilterManager::instance()->toModelIndex(kf); const QModelIndex proxyIndex = d->proxyModel->mapFromSource(sourceIndex); if (proxyIndex.isValid()) { d->combo->setCurrentIndex(proxyIndex.row()); } else { d->combo->setCurrentIndex(0); } } // slot void SearchBar::setChangeStringFilterEnabled(bool on) { d->lineEdit->setEnabled(on); } // slot void SearchBar::setChangeKeyFilterEnabled(bool on) { d->combo->setEnabled(on); } QLineEdit *SearchBar::lineEdit() const { return d->lineEdit; } #include "moc_searchbar.cpp" #include "searchbar.moc"