diff --git a/CMakeLists.txt b/CMakeLists.txt index 814b9b320..f6e88618e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,256 +1,259 @@ # SPDX-FileCopyrightText: none # SPDX-License-Identifier: BSD-3-Clause cmake_minimum_required(VERSION 3.16 FATAL_ERROR) set(RELEASE_SERVICE_VERSION_MAJOR "24") set(RELEASE_SERVICE_VERSION_MINOR "01") set(RELEASE_SERVICE_VERSION_MICRO "90") # The RELEASE_SERVICE_VERSION is used by Gpg4win to add the Gpg4win version if (NOT RELEASE_SERVICE_VERSION) set(RELEASE_SERVICE_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}.${RELEASE_SERVICE_VERSION_MINOR}.${RELEASE_SERVICE_VERSION_MICRO}") endif() if(RELEASE_SERVICE_VERSION_MICRO LESS 10) set(KDE_APPLICATIONS_COMPACT_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}${RELEASE_SERVICE_VERSION_MINOR}0${RELEASE_SERVICE_VERSION_MICRO}") else() set(KDE_APPLICATIONS_COMPACT_VERSION "${RELEASE_SERVICE_VERSION_MAJOR}${RELEASE_SERVICE_VERSION_MINOR}${RELEASE_SERVICE_VERSION_MICRO}") endif() set(KLEOPATRA_VERSION_MAJOR "3") -set(KLEOPATRA_VERSION_MINOR "1") -set(KLEOPATRA_VERSION_MICRO "28") +set(KLEOPATRA_VERSION_MINOR "2") +set(KLEOPATRA_VERSION_MICRO "0") set(kleopatra_version "${KLEOPATRA_VERSION_MAJOR}.${KLEOPATRA_VERSION_MINOR}.${KLEOPATRA_VERSION_MICRO}.${KDE_APPLICATIONS_COMPACT_VERSION}") # The following is for Windows set(kleopatra_version_win "${KLEOPATRA_VERSION_MAJOR}.${KLEOPATRA_VERSION_MINOR}.${KLEOPATRA_VERSION_MICRO}") set(kleopatra_fileversion_win "${KLEOPATRA_VERSION_MAJOR},${KLEOPATRA_VERSION_MINOR},${KLEOPATRA_VERSION_MICRO},0") if (NOT KLEOPATRA_DISTRIBUTION_TEXT) # This is only used on Windows for the file attributes of Kleopatra set(KLEOPATRA_DISTRIBUTION_TEXT "KDE") endif() project(kleopatra VERSION ${kleopatra_version}) option(DISABLE_KWATCHGNUPG "Don't build the kwatchgnupg tool [default=OFF]" OFF) # Standalone build. Find / include everything necessary. set(KF_MIN_VERSION "5.246.0") set(KIDENTITYMANAGEMENT_VERSION "5.240.95") set(KMAILTRANSPORT_VERSION "5.240.95") set(AKONADI_MIME_VERSION "5.240.95") set(KMIME_VERSION "5.240.95") set(LIBKLEO_VERSION "5.240.50") set(QT_REQUIRED_VERSION "6.6.0") set(MIMETREEPARSER_VERSION "5.240.95") set(GPGME_REQUIRED_VERSION "1.20.0") set(LIBASSUAN_REQUIRED_VERSION "2.4.2") set(GPG_ERROR_REQUIRED_VERSION "1.36") if (WIN32) set(KF6_WANT_VERSION "5.240.95") set(KMIME_WANT_VERSION "5.246.0") else () set(KF6_WANT_VERSION ${KF_MIN_VERSION}) set(KMIME_WANT_VERSION ${KMIME_VERSION}) endif () set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(ECM ${KF6_WANT_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH}) include(ECMInstallIcons) include(ECMSetupVersion) include(ECMAddTests) include(GenerateExportHeader) include(ECMGenerateHeaders) include(FeatureSummary) include(CheckFunctionExists) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) include(ECMDeprecationSettings) include(KDEClangFormat) include(KDEGitCommitHooks) # Find KF6 packages find_package(KF6 ${KF6_WANT_VERSION} REQUIRED COMPONENTS Codecs Config CoreAddons Crash I18n IconThemes ItemModels KCMUtils KIO WidgetsAddons WindowSystem XmlGui StatusNotifierItem OPTIONAL_COMPONENTS DocTools ) set_package_properties(KF6DocTools PROPERTIES DESCRIPTION "Documentation tools" PURPOSE "Required to generate Kleopatra documentation." TYPE OPTIONAL) # Optional packages if (WIN32) # Only a replacement available for Windows so this # is required on other platforms. find_package(KF6DBusAddons ${KF6_WANT_VERSION} CONFIG) set_package_properties(KF6DBusAddons PROPERTIES DESCRIPTION "Support library to work with DBus" PURPOSE "DBus session integration" URL "https://inqlude.org/libraries/kdbusaddons.html" TYPE OPTIONAL) else() find_package(KF6DBusAddons ${KF6_WANT_VERSION} CONFIG REQUIRED) set(_kleopatra_dbusaddons_libs KF6::DBusAddons) endif() set(HAVE_QDBUS ${Qt6DBus_FOUND}) find_package(Gpgmepp ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) set(QGPGME_NAME "QGpgmeQt6") find_package(${QGPGME_NAME} ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.21.0") set(QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME 1) set(QGPGME_ARCHIVE_JOBS_SUPPORT_INPUT_FILENAME 1) endif() if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.22.0") set(QGPGME_HAS_TOLOGSTRING 1) set(QGPGME_SUPPORTS_IS_MIME 1) endif() if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.23.0") set(QGPGME_SUPPORTS_WKD_REFRESH_JOB 1) endif() +if (${QGPGME_NAME}_VERSION VERSION_GREATER_EQUAL "1.23.3") + set(QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO 1) +endif() find_package(KPim6Libkleo ${LIBKLEO_VERSION} CONFIG REQUIRED) find_package(KPim6Mime ${KMIME_WANT_VERSION} CONFIG REQUIRED) find_package(KPim6IdentityManagementCore ${KIDENTITYMANAGEMENT_VERSION} CONFIG) find_package(KPim6MailTransport ${KMAILTRANSPORT_VERSION} CONFIG) find_package(KPim6AkonadiMime ${AKONADI_MIME_VERSION} CONFIG) find_package(KPim6MimeTreeParserWidgets ${MIMETREEPARSER_VERSION} CONFIG REQUIRED) find_package(Qt6 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Widgets Test Network PrintSupport) find_package(LibAssuan ${LIBASSUAN_REQUIRED_VERSION} REQUIRED) set_package_properties(LibAssuan PROPERTIES TYPE REQUIRED PURPOSE "Needed for Kleopatra to act as the GnuPG UI Server" ) find_package(LibGpgError ${GPG_ERROR_REQUIRED_VERSION} REQUIRED) set_package_properties(LibGpgError PROPERTIES TYPE REQUIRED ) set(kleopatra_release FALSE) if(NOT kleopatra_release) find_package(Git) if(GIT_FOUND) execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} RESULT_VARIABLE rc ERROR_QUIET) if(rc EQUAL 0) execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --oneline --format=%h ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE Kleopatra_WC_REVISION) string(REGEX REPLACE "\n" "" Kleopatra_WC_REVISION "${Kleopatra_WC_REVISION}") execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --oneline --format=%cI ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE Kleopatra_WC_LAST_CHANGED_DATE) string(REGEX REPLACE "^([0-9]+)-([0-9]+)-([0-9]+)T([0-9]+):([0-9]+):([0-9]+).*$" "\\1\\2\\3T\\4\\5\\6" Kleopatra_WC_LAST_CHANGED_DATE "${Kleopatra_WC_LAST_CHANGED_DATE}") set(kleopatra_version "${kleopatra_version}+git${Kleopatra_WC_LAST_CHANGED_DATE}~${Kleopatra_WC_REVISION}") endif() endif() endif() configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version-kleopatra.h.in ${CMAKE_CURRENT_BINARY_DIR}/version-kleopatra.h) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-kleopatra.h.in ${CMAKE_CURRENT_BINARY_DIR}/config-kleopatra.h) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ) if (WIN32) # On Windows, we need to use stuff deprecated since Qt 5.11, e.g. from QDesktopWidget add_definitions(-DQT_NO_CONTEXTLESS_CONNECT) ecm_set_disabled_deprecation_versions(QT 5.10.0 KF 5.248.0) else () add_definitions(-DQT_NO_CONTEXTLESS_CONNECT) ecm_set_disabled_deprecation_versions(QT 6.6.0 KF 5.248.0) endif () if(CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-missing-braces -Wno-parentheses -Wno-ignored-qualifiers") endif() if(MINGW) # we do not care about different signedness of passed pointer arguments add_compile_options($<$:-Wno-pointer-sign>) endif() add_definitions(-DQT_NO_EMIT) remove_definitions(-DQT_NO_FOREACH) # Disable the use of QStringBuilder for operator+ to prevent crashes when # returning the result of concatenating string temporaries in lambdas. We do # this for example in some std::transform expressions. # This is a known issue: https://bugreports.qt.io/browse/QTBUG-47066 # Alternatively, one would always have to remember to force the lambdas to # return a QString instead of QStringBuilder, but that's just too easy to # forget and, unfortunately, the compiler doesn't issue a warning if one forgets # this. So, it's just too dangerous. # One can still use QStringBuilder explicitly with the operator% if necessary. remove_definitions(-DQT_USE_FAST_OPERATOR_PLUS) remove_definitions(-DQT_USE_QSTRINGBUILDER) kde_enable_exceptions() option(USE_UNITY_CMAKE_SUPPORT "Use UNITY cmake support (speedup compile time)" OFF) set(COMPILE_WITH_UNITY_CMAKE_SUPPORT OFF) if (USE_UNITY_CMAKE_SUPPORT) set(COMPILE_WITH_UNITY_CMAKE_SUPPORT ON) endif() add_subdirectory(pics) add_subdirectory(src) if(BUILD_TESTING) add_subdirectory(tests) add_subdirectory(autotests) endif() ecm_qt_install_logging_categories( EXPORT KLEOPATRA FILE kleopatra.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ) ki18n_install(po) if(KF6DocTools_FOUND) kdoctools_install(po) add_subdirectory(doc) endif() feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) # add clang-format target for all our real source files file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h *.c) kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) diff --git a/config-kleopatra.h.in b/config-kleopatra.h.in index 7db43c96b..bf49438a8 100644 --- a/config-kleopatra.h.in +++ b/config-kleopatra.h.in @@ -1,17 +1,20 @@ /* DBus available */ #cmakedefine01 HAVE_QDBUS /* Whether the archive jobs allow setting an output filename instead of passing an output IO device */ #cmakedefine01 QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME /* Whether the archive jobs allow setting an input filename instead of passing an input IO device */ #cmakedefine01 QGPGME_ARCHIVE_JOBS_SUPPORT_INPUT_FILENAME /* Whether QGpgMe supports isMime on an decryptionResult */ #cmakedefine01 QGPGME_SUPPORTS_IS_MIME /* Whether QGpgME provides the toLogString helper */ #cmakedefine01 QGPGME_HAS_TOLOGSTRING /* Whether QGpgME supports the WKD refresh job */ #cmakedefine01 QGPGME_SUPPORTS_WKD_REFRESH_JOB + +/* Whether the normal encrypt/decrypt/sign/verify jobs support direct file IO */ +#cmakedefine01 QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO diff --git a/src/aboutdata.cpp b/src/aboutdata.cpp index 5f5dcc84f..739ef67b9 100644 --- a/src/aboutdata.cpp +++ b/src/aboutdata.cpp @@ -1,156 +1,154 @@ /* aboutdata.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2001, 2002, 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #include "aboutdata.h" #include "kleopatraapplication.h" #include #include #include #include #include #include #include "kleopatra_debug.h" /* Path to GnuPGs signing keys relative to the GnuPG installation */ #ifndef GNUPG_DISTSIGKEY_RELPATH #define GNUPG_DISTSIGKEY_RELPATH "/../share/gnupg/distsigkey.gpg" #endif /* Path to a VERSION file relative to QCoreApplication::applicationDirPath */ #ifndef VERSION_RELPATH #define VERSION_RELPATH "/../VERSION" #endif static const char kleopatra_version[] = KLEOPATRA_VERSION_STRING; struct about_data { const KLazyLocalizedString name; const KLazyLocalizedString desc; const char *email; const char *web; }; static const about_data authors[] = { {kli18n("Andre Heinecke"), kli18n("Current Maintainer"), "aheinecke@gnupg.org", nullptr}, {kli18n("Marc Mutz"), kli18n("Former Maintainer"), "mutz@kde.org", nullptr}, {kli18n("Steffen Hansen"), kli18n("Former Maintainer"), "hansen@kde.org", nullptr}, {kli18n("Matthias Kalle Dalheimer"), kli18n("Original Author"), "kalle@kde.org", nullptr}, }; static const about_data credits[] = { {kli18n("David Faure"), kli18n("Backend configuration framework, KIO integration"), "faure@kde.org", nullptr}, {kli18n("Michel Boyer de la Giroday"), kli18n("Key-state dependent colors and fonts in the certificates list"), "michel@klaralvdalens-datakonsult.se", nullptr}, {kli18n("Thomas Moenicke"), kli18n("Artwork"), "tm@php-qt.org", nullptr}, {kli18n("Frank Osterfeld"), kli18n("Resident gpgme/win wrangler, UI Server commands and dialogs"), "osterfeld@kde.org", nullptr}, {kli18n("Karl-Heinz Zimmer"), kli18n("DN display ordering support, infrastructure"), "khz@kde.org", nullptr}, {kli18n("Laurent Montel"), kli18n("Qt5 port, general code maintenance"), "montel@kde.org", nullptr}, }; -void updateAboutDataFromSettings(const QSettings *settings) +static void updateAboutDataFromSettings(KAboutData *about, const QSettings *settings) { - if (!settings) { + if (!about || !settings) { return; } - auto about = KAboutData::applicationData(); - about.setDisplayName(settings->value(QStringLiteral("displayName"), about.displayName()).toString()); - about.setProductName(settings->value(QStringLiteral("productName"), about.productName()).toByteArray()); - about.setComponentName(settings->value(QStringLiteral("componentName"), about.componentName()).toString()); - about.setShortDescription(settings->value(QStringLiteral("shortDescription"), about.shortDescription()).toString()); - about.setHomepage(settings->value(QStringLiteral("homepage"), about.homepage()).toString()); - about.setBugAddress(settings->value(QStringLiteral("bugAddress"), about.bugAddress()).toByteArray()); - about.setVersion(settings->value(QStringLiteral("version"), about.version()).toByteArray()); - about.setOtherText(settings->value(QStringLiteral("otherText"), about.otherText()).toString()); - about.setCopyrightStatement(settings->value(QStringLiteral("copyrightStatement"), about.copyrightStatement()).toString()); - about.setDesktopFileName(settings->value(QStringLiteral("desktopFileName"), about.desktopFileName()).toString()); - KAboutData::setApplicationData(about); + about->setDisplayName(settings->value(QStringLiteral("displayName"), about->displayName()).toString()); + about->setProductName(settings->value(QStringLiteral("productName"), about->productName()).toByteArray()); + about->setComponentName(settings->value(QStringLiteral("componentName"), about->componentName()).toString()); + about->setShortDescription(settings->value(QStringLiteral("shortDescription"), about->shortDescription()).toString()); + about->setHomepage(settings->value(QStringLiteral("homepage"), about->homepage()).toString()); + about->setBugAddress(settings->value(QStringLiteral("bugAddress"), about->bugAddress()).toByteArray()); + about->setVersion(settings->value(QStringLiteral("version"), about->version()).toByteArray()); + about->setOtherText(settings->value(QStringLiteral("otherText"), about->otherText()).toString()); + about->setCopyrightStatement(settings->value(QStringLiteral("copyrightStatement"), about->copyrightStatement()).toString()); + about->setDesktopFileName(settings->value(QStringLiteral("desktopFileName"), about->desktopFileName()).toString()); } // Extend the about data with the used GnuPG Version since this can // make a big difference with regards to the available features. static void loadBackendVersions() { auto thread = QThread::create([]() { STARTUP_TIMING << "Checking backend versions"; const auto backendVersions = Kleo::backendVersionInfo(); STARTUP_TIMING << "backend versions checked"; if (!backendVersions.empty()) { QMetaObject::invokeMethod(qApp, [backendVersions]() { auto about = KAboutData::applicationData(); about.setOtherText(i18nc("Preceeds a list of applications/libraries used by Kleopatra", "Uses:") // + QLatin1String{"
  • "} // + backendVersions.join(QLatin1String{"
  • "}) // + QLatin1String{"
"} // + about.otherText()); KAboutData::setApplicationData(about); }); } }); thread->start(); } // This code is mostly for Gpg4win and GnuPG VS-Desktop so that they // can put in their own about data information. -static void loadCustomAboutData() +static void loadCustomAboutData(KAboutData *about) { const QStringList searchPaths = {Kleo::gnupgInstallPath()}; const QString versionFile = QCoreApplication::applicationDirPath() + QStringLiteral(VERSION_RELPATH); const QString distSigKeys = Kleo::gnupgInstallPath() + QStringLiteral(GNUPG_DISTSIGKEY_RELPATH); STARTUP_TIMING << "Starting version info check"; bool valid = Kleo::gpgvVerify(versionFile, QString(), distSigKeys, searchPaths); STARTUP_TIMING << "Version info checked"; if (valid) { qCDebug(KLEOPATRA_LOG) << "Found valid VERSION file. Updating about data."; auto settings = std::make_shared(versionFile, QSettings::IniFormat); settings->beginGroup(QStringLiteral("Kleopatra")); - updateAboutDataFromSettings(settings.get()); + updateAboutDataFromSettings(about, settings.get()); KleopatraApplication::instance()->setDistributionSettings(settings); } loadBackendVersions(); } AboutData::AboutData() : KAboutData(QStringLiteral("kleopatra"), i18n("Kleopatra"), QLatin1String(kleopatra_version), i18n("Certificate Manager and Unified Crypto GUI"), KAboutLicense::GPL, i18n("(c) 2002 Steffen\u00A0Hansen, Matthias\u00A0Kalle\u00A0Dalheimer, Klar\u00E4lvdalens\u00A0Datakonsult\u00A0AB\n" "(c) 2004, 2007, 2008, 2009 Marc\u00A0Mutz, Klar\u00E4lvdalens\u00A0Datakonsult\u00A0AB") // + QLatin1Char('\n') // + i18n("(c) 2016-2018 Intevation GmbH") // + QLatin1Char('\n') // + i18n("(c) 2010-%1 The Kleopatra developers, g10 Code GmbH", QStringLiteral("2024"))) { using ::authors; using ::credits; for (unsigned int i = 0; i < sizeof authors / sizeof *authors; ++i) { addAuthor(KLocalizedString(authors[i].name).toString(), KLocalizedString(authors[i].desc).toString(), QLatin1String(authors[i].email), QLatin1String(authors[i].web)); } for (unsigned int i = 0; i < sizeof credits / sizeof *credits; ++i) { addCredit(KLocalizedString(credits[i].name).toString(), KLocalizedString(credits[i].desc).toString(), QLatin1String(credits[i].email), QLatin1String(credits[i].web)); } - loadCustomAboutData(); + loadCustomAboutData(this); } diff --git a/src/commands/certifycertificatecommand.cpp b/src/commands/certifycertificatecommand.cpp index 475015152..152a02899 100644 --- a/src/commands/certifycertificatecommand.cpp +++ b/src/commands/certifycertificatecommand.cpp @@ -1,338 +1,338 @@ /* -*- mode: c++; c-basic-offset:4 -*- - commands/signcertificatecommand.cpp + 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) { 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/certifycertificatecommand.h b/src/commands/certifycertificatecommand.h index b4aee2cf1..3661d8603 100644 --- a/src/commands/certifycertificatecommand.h +++ b/src/commands/certifycertificatecommand.h @@ -1,51 +1,51 @@ /* -*- mode: c++; c-basic-offset:4 -*- - commands/signcertificatecommand.h + commands/certifycertificatecommand.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include namespace GpgME { class UserID; } namespace Kleo { namespace Commands { class CertifyCertificateCommand : public Command { Q_OBJECT public: explicit CertifyCertificateCommand(QAbstractItemView *view, KeyListController *parent); explicit CertifyCertificateCommand(KeyListController *parent); explicit CertifyCertificateCommand(const GpgME::Key &key); explicit CertifyCertificateCommand(const GpgME::UserID &uid); explicit CertifyCertificateCommand(const std::vector &uids); ~CertifyCertificateCommand() override; /* reimp */ static Restrictions restrictions() { return OnlyOneKey | MustBeOpenPGP | MustBeValid; } 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/changeexpirycommand.cpp b/src/commands/changeexpirycommand.cpp index d95f0d6ca..14b6b99c7 100644 --- a/src/commands/changeexpirycommand.cpp +++ b/src/commands/changeexpirycommand.cpp @@ -1,308 +1,310 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/changeexpirycommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "changeexpirycommand.h" #include "command_p.h" #include "dialogs/expirydialog.h" #include "utils/expiration.h" #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace GpgME; using namespace QGpgME; namespace { bool subkeyHasSameExpirationAsPrimaryKey(const Subkey &subkey) { // we allow for a difference in expiration of up to 10 seconds static const auto maxExpirationDifference = 10; Q_ASSERT(!subkey.isNull()); const auto key = subkey.parent(); const auto primaryKey = key.subkey(0); const auto primaryExpiration = quint32(primaryKey.expirationTime()); const auto subkeyExpiration = quint32(subkey.expirationTime()); if (primaryExpiration != 0 && subkeyExpiration != 0) { return (primaryExpiration == subkeyExpiration) // || ((primaryExpiration > subkeyExpiration) && (primaryExpiration - subkeyExpiration <= maxExpirationDifference)) // || ((primaryExpiration < subkeyExpiration) && (subkeyExpiration - primaryExpiration <= maxExpirationDifference)); } return primaryKey.neverExpires() && subkey.neverExpires(); } bool allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(const Key &key) { Q_ASSERT(!key.isNull() && key.numSubkeys() > 0); const auto subkeys = key.subkeys(); return std::all_of(std::begin(subkeys), std::end(subkeys), [](const auto &subkey) { // revoked subkeys are ignored by gpg --quick-set-expire when updating the expiration of all subkeys; // check if expiration of subkey is (more or less) the same as the expiration of the primary key return subkey.isRevoked() || subkeyHasSameExpirationAsPrimaryKey(subkey); }); } } class ChangeExpiryCommand::Private : public Command::Private { friend class ::Kleo::Commands::ChangeExpiryCommand; ChangeExpiryCommand *q_func() const { return static_cast(q); } public: explicit Private(ChangeExpiryCommand *qq, KeyListController *c); ~Private() override; private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void ensureDialogCreated(ExpiryDialog::Mode mode); void createJob(); void showErrorDialog(const Error &error); void showSuccessDialog(); private: GpgME::Key key; GpgME::Subkey subkey; QPointer dialog; QPointer job; }; ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func() { return static_cast(d.get()); } const ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ChangeExpiryCommand::Private::Private(ChangeExpiryCommand *qq, KeyListController *c) : Command::Private{qq, c} { } ChangeExpiryCommand::Private::~Private() = default; void ChangeExpiryCommand::Private::slotDialogAccepted() { Q_ASSERT(dialog); static const QTime END_OF_DAY{23, 59, 00}; const QDateTime expiry{dialog->dateOfExpiry(), END_OF_DAY}; qCDebug(KLEOPATRA_LOG) << "expiry" << expiry; createJob(); Q_ASSERT(job); std::vector subkeysToUpdate; if (!subkey.isNull()) { // change expiration of a single subkey if (subkey.keyID() != key.keyID()) { // ignore the primary subkey subkeysToUpdate.push_back(subkey); } } else { // change expiration of the (primary) key and, optionally, of some subkeys job->setOptions(ChangeExpiryJob::UpdatePrimaryKey); if (dialog->updateExpirationOfAllSubkeys() && key.numSubkeys() > 1) { // explicitly list the subkeys for which the expiration should be changed // together with the expiration of the (primary) key, so that already expired // subkeys are also updated const auto subkeys = key.subkeys(); std::copy_if(std::next(subkeys.begin()), subkeys.end(), std::back_inserter(subkeysToUpdate), [](const auto &subkey) { // skip revoked subkeys which would anyway be ignored by gpg; // also skip subkeys without explicit expiration because they inherit the primary key's expiration; // include all subkeys that are not yet expired or that expired around the same time as the primary key return !subkey.isRevoked() // && !subkey.neverExpires() // && (!subkey.isExpired() || subkeyHasSameExpirationAsPrimaryKey(subkey)); }); } } if (const Error err = job->start(key, expiry, subkeysToUpdate)) { showErrorDialog(err); finished(); } } void ChangeExpiryCommand::Private::slotDialogRejected() { Q_EMIT q->canceled(); finished(); } void ChangeExpiryCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) ; else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void ChangeExpiryCommand::Private::ensureDialogCreated(ExpiryDialog::Mode mode) { if (dialog) { return; } dialog = new ExpiryDialog{mode}; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void ChangeExpiryCommand::Private::createJob() { Q_ASSERT(!job); const auto backend = (key.protocol() == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); if (!backend) { return; } ChangeExpiryJob *const j = backend->changeExpiryJob(); if (!j) { return; } connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); connect(j, &ChangeExpiryJob::result, q, [this](const auto &err) { slotResult(err); }); job = j; } void ChangeExpiryCommand::Private::showErrorDialog(const Error &err) { error( i18n("

An error occurred while trying to change " "the end of the validity period for %1:

%2

", Formatting::formatForComboBox(key), Formatting::errorAsString(err))); } void ChangeExpiryCommand::Private::showSuccessDialog() { success(i18n("End of validity period changed successfully.")); } ChangeExpiryCommand::ChangeExpiryCommand(KeyListController *c) : Command{new Private{this, c}} { } ChangeExpiryCommand::ChangeExpiryCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{this, c}} { } ChangeExpiryCommand::ChangeExpiryCommand(const GpgME::Key &key) : Command{key, new Private{this, nullptr}} { } ChangeExpiryCommand::~ChangeExpiryCommand() = default; void ChangeExpiryCommand::setSubkey(const GpgME::Subkey &subkey) { d->subkey = subkey; } void ChangeExpiryCommand::doStart() { const std::vector keys = d->keys(); if (keys.size() != 1 // || keys.front().protocol() != GpgME::OpenPGP // || !keys.front().hasSecret() // || keys.front().subkey(0).isNull()) { d->finished(); return; } d->key = keys.front(); if (!d->subkey.isNull() && d->subkey.parent().primaryFingerprint() != d->key.primaryFingerprint()) { qDebug() << "Invalid subkey" << d->subkey.fingerprint() << ": Not a subkey of key" << d->key.primaryFingerprint(); d->finished(); return; } ExpiryDialog::Mode mode; if (!d->subkey.isNull()) { mode = ExpiryDialog::Mode::UpdateIndividualSubkey; } else if (d->key.numSubkeys() == 1) { mode = ExpiryDialog::Mode::UpdateCertificateWithoutSubkeys; } else { mode = ExpiryDialog::Mode::UpdateCertificateWithSubkeys; } d->ensureDialogCreated(mode); Q_ASSERT(d->dialog); const Subkey subkey = !d->subkey.isNull() ? d->subkey : d->key.subkey(0); d->dialog->setDateOfExpiry((subkey.neverExpires() // ? QDate{} // : defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration))); - if (mode == ExpiryDialog::Mode::UpdateCertificateWithSubkeys) { + if (mode == ExpiryDialog::Mode::UpdateIndividualSubkey && subkey.keyID() != subkey.parent().keyID()) { + d->dialog->setPrimaryKey(subkey.parent()); + } else if (mode == ExpiryDialog::Mode::UpdateCertificateWithSubkeys) { d->dialog->setUpdateExpirationOfAllSubkeys(allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(d->key)); } d->dialog->show(); } void ChangeExpiryCommand::doCancel() { if (d->job) { d->job->slotCancel(); } } #undef d #undef q #include "moc_changeexpirycommand.cpp" diff --git a/src/commands/exportopenpgpcertstoservercommand.cpp b/src/commands/exportopenpgpcertstoservercommand.cpp index 9739a6083..fd674cbe5 100644 --- a/src/commands/exportopenpgpcertstoservercommand.cpp +++ b/src/commands/exportopenpgpcertstoservercommand.cpp @@ -1,191 +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) { 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()) { - if (KMessageBox::warningContinueCancel(parent, - xi18nc("@info", - "No OpenPGP directory services have been configured." - "Since none is configured, Kleopatra will use " - "keys.gnupg.net as the server to export to." - "You can configure OpenPGP directory servers in Kleopatra's " - "configuration dialog." - "Do you want to continue with keys.gnupg.net " - "as the server to export to?"), - i18nc("@title:window", "OpenPGP Certificate Export"), - KGuiItem{i18ncp("@action:button", "Export Certificate", "Export Certificates", d->keys().size())}, - KStandardGuiItem::cancel(), - QStringLiteral("warn-export-openpgp-missing-keyserver")) - != KMessageBox::Continue) { - return false; - } + 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(); - if (!haveKeyserverConfigured()) { - result << QStringLiteral("--keyserver") << QStringLiteral("keys.gnupg.net"); - } result << QStringLiteral("--send-keys"); const auto keys = d->keys(); for (const Key &key : keys) { result << QLatin1String(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/lookupcertificatescommand.cpp b/src/commands/lookupcertificatescommand.cpp index df5c21344..afb53f446 100644 --- a/src/commands/lookupcertificatescommand.cpp +++ b/src/commands/lookupcertificatescommand.cpp @@ -1,589 +1,598 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/lookupcertificatescommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008, 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "lookupcertificatescommand.h" #include "importcertificatescommand_p.h" #include "detailscommand.h" #include #include "view/tabwidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace GpgME; using namespace QGpgME; class LookupCertificatesCommand::Private : public ImportCertificatesCommand::Private { friend class ::Kleo::Commands::LookupCertificatesCommand; LookupCertificatesCommand *q_func() const { return static_cast(q); } public: explicit Private(LookupCertificatesCommand *qq, KeyListController *c); ~Private() override; void init(); private: void slotSearchTextChanged(const QString &str); void slotNextKey(const Key &key); void slotKeyListResult(const KeyListResult &result); void slotWKDLookupResult(const WKDLookupResult &result); void tryToFinishKeyLookup(); void slotImportRequested(const std::vector &keys); void slotDetailsRequested(const Key &key); void slotSaveAsRequested(const std::vector &keys); void slotDialogRejected() { canceled(); } private: using ImportCertificatesCommand::Private::showError; void showError(QWidget *parent, const KeyListResult &result); void showResult(QWidget *parent, const KeyListResult &result); void createDialog(); KeyListJob *createKeyListJob(GpgME::Protocol proto) const { const auto cbp = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); return cbp ? cbp->keyListJob(true) : nullptr; } WKDLookupJob *createWKDLookupJob() const { const auto cbp = QGpgME::openpgp(); return cbp ? cbp->wkdLookupJob() : nullptr; } ImportFromKeyserverJob *createImportJob(GpgME::Protocol proto) const { const auto cbp = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); return cbp ? cbp->importFromKeyserverJob() : nullptr; } void startKeyListJob(GpgME::Protocol proto, const QString &str); void startWKDLookupJob(const QString &str); bool checkConfig() const; QWidget *dialogOrParentWidgetOrView() const { if (dialog) { return dialog; } else { return parentWidgetOrView(); } } private: GpgME::Protocol protocol = GpgME::UnknownProtocol; QString query; bool autoStartLookup = false; QPointer dialog; struct KeyListingVariables { QPointer cms, openpgp; QPointer wkdJob; QString pattern; KeyListResult result; std::vector keys; int numKeysWithoutUserId = 0; std::set wkdKeyFingerprints; QByteArray wkdKeyData; QString wkdSource; bool cmsKeysHaveNoFingerprints = false; bool openPgpKeysHaveNoFingerprints = false; void reset() { *this = KeyListingVariables(); } } keyListing; }; LookupCertificatesCommand::Private *LookupCertificatesCommand::d_func() { return static_cast(d.get()); } const LookupCertificatesCommand::Private *LookupCertificatesCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() LookupCertificatesCommand::Private::Private(LookupCertificatesCommand *qq, KeyListController *c) : ImportCertificatesCommand::Private(qq, c) , dialog() { if (!Settings{}.cmsEnabled()) { protocol = GpgME::OpenPGP; } } LookupCertificatesCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG); delete dialog; } LookupCertificatesCommand::LookupCertificatesCommand(KeyListController *c) : ImportCertificatesCommand(new Private(this, c)) { d->init(); } LookupCertificatesCommand::LookupCertificatesCommand(const QString &query, KeyListController *c) : ImportCertificatesCommand(new Private(this, c)) { d->init(); d->query = query; d->autoStartLookup = true; } LookupCertificatesCommand::LookupCertificatesCommand(QAbstractItemView *v, KeyListController *c) : ImportCertificatesCommand(v, new Private(this, c)) { d->init(); if (c->tabWidget()) { d->query = c->tabWidget()->stringFilter(); // do not start the lookup automatically to prevent unwanted leaking // of information } } void LookupCertificatesCommand::Private::init() { } LookupCertificatesCommand::~LookupCertificatesCommand() { qCDebug(KLEOPATRA_LOG); } void LookupCertificatesCommand::setProtocol(GpgME::Protocol protocol) { d->protocol = protocol; } GpgME::Protocol LookupCertificatesCommand::protocol() const { return d->protocol; } void LookupCertificatesCommand::doStart() { if (!d->checkConfig()) { d->finished(); return; } d->createDialog(); Q_ASSERT(d->dialog); // if we have a prespecified query, load it into find field // and start the search, if auto-start is enabled if (!d->query.isEmpty()) { d->dialog->setSearchText(d->query); if (d->autoStartLookup) { d->slotSearchTextChanged(d->query); } } else { d->dialog->setPassive(false); } d->dialog->show(); } void LookupCertificatesCommand::Private::createDialog() { if (dialog) { return; } dialog = new LookupCertificatesDialog; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); + + const bool wkdOnly = !haveKeyserverConfigured() && !haveX509DirectoryServerConfigured(); + dialog->setQueryMode(wkdOnly ? LookupCertificatesDialog::EmailQuery : LookupCertificatesDialog::AnyQuery); + connect(dialog, &LookupCertificatesDialog::searchTextChanged, q, [this](const QString &text) { slotSearchTextChanged(text); }); using CertsVec = std::vector; connect(dialog, &LookupCertificatesDialog::saveAsRequested, q, [this](const CertsVec &certs) { slotSaveAsRequested(certs); }); connect(dialog, &LookupCertificatesDialog::importRequested, q, [this](const CertsVec &certs) { slotImportRequested(certs); }); connect(dialog, &LookupCertificatesDialog::detailsRequested, q, [this](const GpgME::Key &gpgKey) { slotDetailsRequested(gpgKey); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } static auto searchTextToEmailAddress(const QString &s) { return QString::fromStdString(UserID::addrSpecFromString(s.toStdString().c_str())); } void LookupCertificatesCommand::Private::slotSearchTextChanged(const QString &str) { // pressing return might trigger both search and dialog destruction (search focused and default key set) // On Windows, the dialog is then destroyed before this slot is called if (dialog) { // thus test dialog->setPassive(true); dialog->setCertificates(std::vector()); dialog->showInformation({}); } - query = str; - keyListing.reset(); keyListing.pattern = str; if (protocol != GpgME::OpenPGP) { startKeyListJob(CMS, str); } if (protocol != GpgME::CMS) { - static const QRegularExpression rx(QRegularExpression::anchoredPattern(QLatin1String("(?:0x|0X)?[0-9a-fA-F]{6,}"))); - if (rx.match(query).hasMatch() && !str.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) { - qCDebug(KLEOPATRA_LOG) << "Adding 0x prefix to query"; - startKeyListJob(OpenPGP, QStringLiteral("0x") + str); + static const QRegularExpression rx(QRegularExpression::anchoredPattern(QLatin1String("[0-9a-fA-F]{6,}"))); + if (rx.match(str).hasMatch()) { + qCDebug(KLEOPATRA_LOG) << "Adding 0x prefix to query" << str; + startKeyListJob(OpenPGP, QLatin1String{"0x"} + str); } else { startKeyListJob(OpenPGP, str); - if (str.contains(QLatin1Char{'@'}) && !searchTextToEmailAddress(str).isEmpty()) { - startWKDLookupJob(str); - } + } + if (str.contains(QLatin1Char{'@'}) && !searchTextToEmailAddress(str).isEmpty()) { + startWKDLookupJob(str); } } } void LookupCertificatesCommand::Private::startKeyListJob(GpgME::Protocol proto, const QString &str) { + if ((proto == GpgME::OpenPGP) && !haveKeyserverConfigured()) { + // avoid starting an OpenPGP key server lookup if key server usage has been disabled; + // for S/MIME we start the job regardless of configured directory servers to account for + // dirmngr knowing better than our check for directory servers + return; + } + KeyListJob *const klj = createKeyListJob(proto); if (!klj) { return; } connect(klj, &QGpgME::KeyListJob::result, q, [this](const GpgME::KeyListResult &result) { slotKeyListResult(result); }); connect(klj, &QGpgME::KeyListJob::nextKey, q, [this](const GpgME::Key &key) { slotNextKey(key); }); if (const Error err = klj->start(QStringList(str))) { keyListing.result.mergeWith(KeyListResult(err)); } else if (proto == CMS) { keyListing.cms = klj; } else { keyListing.openpgp = klj; } } void LookupCertificatesCommand::Private::startWKDLookupJob(const QString &str) { const auto job = createWKDLookupJob(); if (!job) { qCDebug(KLEOPATRA_LOG) << "Failed to create WKDLookupJob"; return; } connect(job, &WKDLookupJob::result, q, [this](const WKDLookupResult &result) { slotWKDLookupResult(result); }); if (const Error err = job->start(str)) { keyListing.result.mergeWith(KeyListResult{err}); } else { keyListing.wkdJob = job; } } void LookupCertificatesCommand::Private::slotNextKey(const Key &key) { if (!key.primaryFingerprint()) { qCDebug(KLEOPATRA_LOG) << __func__ << "ignoring key without fingerprint" << key; if (q->sender() == keyListing.cms) { keyListing.cmsKeysHaveNoFingerprints = true; } else if (q->sender() == keyListing.openpgp) { keyListing.openPgpKeysHaveNoFingerprints = true; } } else if (key.numUserIDs() == 0) { qCDebug(KLEOPATRA_LOG) << __func__ << "ignoring key without user IDs" << key; keyListing.numKeysWithoutUserId++; } else { qCDebug(KLEOPATRA_LOG) << __func__ << "got key" << key; keyListing.keys.push_back(key); } } void LookupCertificatesCommand::Private::slotKeyListResult(const KeyListResult &r) { if (q->sender() == keyListing.cms) { keyListing.cms = nullptr; } else if (q->sender() == keyListing.openpgp) { keyListing.openpgp = nullptr; } else { qCDebug(KLEOPATRA_LOG) << "unknown sender()" << q->sender(); } keyListing.result.mergeWith(r); tryToFinishKeyLookup(); } static auto removeKeysNotMatchingEmail(const std::vector &keys, const std::string &email) { std::vector filteredKeys; const auto addrSpec = UserID::addrSpecFromString(email.c_str()); std::copy_if(std::begin(keys), std::end(keys), std::back_inserter(filteredKeys), [addrSpec](const auto &key) { const auto uids = key.userIDs(); return std::any_of(std::begin(uids), std::end(uids), [addrSpec](const auto &uid) { return uid.addrSpec() == addrSpec; }); }); return filteredKeys; } void LookupCertificatesCommand::Private::slotWKDLookupResult(const WKDLookupResult &result) { if (q->sender() == keyListing.wkdJob) { keyListing.wkdJob = nullptr; } else { qCDebug(KLEOPATRA_LOG) << __func__ << "unknown sender()" << q->sender(); } // we do not want to bother the user with errors during the WKD lookup; // therefore, we log the result, but we do not merge it into keyListing.result qCDebug(KLEOPATRA_LOG) << "Result of WKD lookup:" << result.error(); const auto keys = removeKeysNotMatchingEmail(result.keyData().toKeys(GpgME::OpenPGP), result.pattern()); if (!keys.empty()) { keyListing.wkdKeyData = QByteArray::fromStdString(result.keyData().toString()); keyListing.wkdSource = QString::fromStdString(result.source()); std::copy(std::begin(keys), std::end(keys), std::back_inserter(keyListing.keys)); // remember the keys retrieved via WKD for import std::transform(std::begin(keys), std::end(keys), std::inserter(keyListing.wkdKeyFingerprints, std::begin(keyListing.wkdKeyFingerprints)), [](const auto &k) { return k.primaryFingerprint(); }); } tryToFinishKeyLookup(); } namespace { void showKeysWithoutFingerprintsNotification(QWidget *parent, GpgME::Protocol protocol) { if (protocol != GpgME::CMS && protocol != GpgME::OpenPGP) { return; } QString message; if (protocol == GpgME::CMS) { message = xi18nc("@info", "One of the X.509 directory services returned certificates without " "fingerprints. Those certificates are ignored because fingerprints " "are required as unique identifiers for certificates." "You may want to configure a different X.509 directory service " "in the configuration dialog."); } else { message = xi18nc("@info", "The OpenPGP keyserver returned certificates without " "fingerprints. Those certificates are ignored because fingerprints " "are required as unique identifiers for certificates." "You may want to configure a different OpenPGP keyserver " "in the configuration dialog."); } KMessageBox::information(parent, message, i18nc("@title", "Invalid Server Reply"), QStringLiteral("certificates-lookup-missing-fingerprints")); } } void LookupCertificatesCommand::Private::tryToFinishKeyLookup() { if (keyListing.cms || keyListing.openpgp || keyListing.wkdJob) { // still waiting for jobs to complete return; } if (keyListing.result.error() && !keyListing.result.error().isCanceled()) { showError(dialog, keyListing.result); } if (keyListing.result.isTruncated()) { showResult(dialog, keyListing.result); } if (keyListing.cmsKeysHaveNoFingerprints) { showKeysWithoutFingerprintsNotification(dialog, GpgME::CMS); } if (keyListing.openPgpKeysHaveNoFingerprints) { showKeysWithoutFingerprintsNotification(dialog, GpgME::OpenPGP); } if (dialog) { dialog->setPassive(false); dialog->setCertificates(keyListing.keys); if (keyListing.numKeysWithoutUserId > 0) { dialog->showInformation(i18ncp("@info", "One certificate without name and email address was ignored.", "%1 certificates without name and email address were ignored.", keyListing.numKeysWithoutUserId)); } } else { finished(); } } void LookupCertificatesCommand::Private::slotImportRequested(const std::vector &keys) { dialog = nullptr; Q_ASSERT(!keys.empty()); Q_ASSERT(std::none_of(keys.cbegin(), keys.cend(), [](const Key &key) { return key.isNull(); })); std::vector wkdKeys, otherKeys; otherKeys.reserve(keys.size()); kdtools::separate_if(std::begin(keys), std::end(keys), std::back_inserter(wkdKeys), std::back_inserter(otherKeys), [this](const auto &key) { return key.primaryFingerprint() && keyListing.wkdKeyFingerprints.find(key.primaryFingerprint()) != std::end(keyListing.wkdKeyFingerprints); }); std::vector pgp, cms; pgp.reserve(otherKeys.size()); cms.reserve(otherKeys.size()); kdtools::separate_if(otherKeys.begin(), otherKeys.end(), std::back_inserter(pgp), std::back_inserter(cms), [](const Key &key) { return key.protocol() == GpgME::OpenPGP; }); setWaitForMoreJobs(true); if (!wkdKeys.empty()) { // set an import filter, so that only user IDs matching the email address used for the WKD lookup are imported const QString importFilter = QLatin1String{"keep-uid=mbox = "} + searchTextToEmailAddress(keyListing.pattern); startImport(OpenPGP, keyListing.wkdKeyData, keyListing.wkdSource, {importFilter, Key::OriginWKD, keyListing.wkdSource}); } if (!pgp.empty()) { startImport(OpenPGP, pgp, i18nc(R"(@title %1:"OpenPGP" or "S/MIME")", "%1 Certificate Server", Formatting::displayName(OpenPGP))); } if (!cms.empty()) { startImport(CMS, cms, i18nc(R"(@title %1:"OpenPGP" or "S/MIME")", "%1 Certificate Server", Formatting::displayName(CMS))); } setWaitForMoreJobs(false); } void LookupCertificatesCommand::Private::slotSaveAsRequested(const std::vector &keys) { Q_UNUSED(keys) qCDebug(KLEOPATRA_LOG) << "not implemented"; } void LookupCertificatesCommand::Private::slotDetailsRequested(const Key &key) { Command *const cmd = new DetailsCommand(key); cmd->setParentWidget(dialogOrParentWidgetOrView()); cmd->start(); } void LookupCertificatesCommand::doCancel() { ImportCertificatesCommand::doCancel(); if (QDialog *const dlg = d->dialog) { d->dialog = nullptr; dlg->close(); } } void LookupCertificatesCommand::Private::showError(QWidget *parent, const KeyListResult &result) { if (!result.error()) { return; } KMessageBox::information(parent, i18nc("@info", "Failed to search on certificate server. The error returned was:\n%1", Formatting::errorAsString(result.error()))); } void LookupCertificatesCommand::Private::showResult(QWidget *parent, const KeyListResult &result) { if (result.isTruncated()) KMessageBox::information(parent, xi18nc("@info", "The query result has been truncated." "Either the local or a remote limit on " "the maximum number of returned hits has " "been exceeded." "You can try to increase the local limit " "in the configuration dialog, but if one " "of the configured servers is the limiting " "factor, you have to refine your search."), i18nc("@title", "Result Truncated"), QStringLiteral("lookup-certificates-truncated-result")); } bool LookupCertificatesCommand::Private::checkConfig() const { - const bool haveOrDontNeedOpenPGPServer = haveKeyserverConfigured() || (protocol == GpgME::CMS); - const bool haveOrDontNeedCMSServer = haveX509DirectoryServerConfigured() || (protocol == GpgME::OpenPGP); - const bool ok = haveOrDontNeedOpenPGPServer || haveOrDontNeedCMSServer; - if (!ok) + // unless CMS-only lookup is requested we always try a lookup via WKD + const bool ok = (protocol != GpgME::CMS) || haveX509DirectoryServerConfigured(); + if (!ok) { information(xi18nc("@info", "You do not have any directory servers configured." "You need to configure at least one directory server to " "search on one." "You can configure directory servers here: " "Settings->Configure Kleopatra."), i18nc("@title", "No Directory Servers Configured")); + } return ok; } #undef d #undef q #include "moc_lookupcertificatescommand.cpp" diff --git a/src/commands/refreshopenpgpcertscommand.cpp b/src/commands/refreshopenpgpcertscommand.cpp index 59bb29ed4..13f6e6fa0 100644 --- a/src/commands/refreshopenpgpcertscommand.cpp +++ b/src/commands/refreshopenpgpcertscommand.cpp @@ -1,119 +1,105 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/refreshopenpgpcertscommand.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 "refreshopenpgpcertscommand.h" +#include "command_p.h" + #include #include #include using namespace Kleo; using namespace Kleo::Commands; RefreshOpenPGPCertsCommand::RefreshOpenPGPCertsCommand(KeyListController *c) : GnuPGProcessCommand(c) { setShowsOutputWindow(true); } RefreshOpenPGPCertsCommand::RefreshOpenPGPCertsCommand(QAbstractItemView *v, KeyListController *c) : GnuPGProcessCommand(v, c) { setShowsOutputWindow(true); } RefreshOpenPGPCertsCommand::~RefreshOpenPGPCertsCommand() { } bool RefreshOpenPGPCertsCommand::preStartHook(QWidget *parent) const { - if (!haveKeyserverConfigured()) - if (KMessageBox::warningContinueCancel(parent, - xi18nc("@info", - "No OpenPGP directory services have been configured." - "If not all of the certificates carry the name of their preferred " - "certificate server (few do), a fallback server is needed to fetch from." - "Since none is configured, Kleopatra will use " - "keys.gnupg.net as the fallback." - "You can configure OpenPGP directory servers in Kleopatra's " - "configuration dialog." - "Do you want to continue with keys.gnupg.net " - "as fallback server?"), - i18nc("@title:window", "OpenPGP Certificate Refresh"), - KStandardGuiItem::cont(), - KStandardGuiItem::cancel(), - QStringLiteral("warn-refresh-openpgp-missing-keyserver")) - != KMessageBox::Continue) { - return false; - } + if (!haveKeyserverConfigured()) { + d->error(i18nc("@info", + "Refreshing the OpenPGP certificates is not possible because " + "the usage of key servers has been disabled explicitly.")); + return false; + } return KMessageBox::warningContinueCancel(parent, xi18nc("@info", "Refreshing OpenPGP certificates implies downloading all certificates anew, " "to check if any of them have been revoked in the meantime." "This can put a severe strain on your own as well as other people's network " "connections, and can take up to an hour or more to complete, depending on " "your network connection, and the number of certificates to check. " "Are you sure you want to continue?"), i18nc("@title:window", "OpenPGP Certificate Refresh"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("warn-refresh-openpgp-expensive")) == KMessageBox::Continue; } QStringList RefreshOpenPGPCertsCommand::arguments() const { QStringList result; result << gpgPath(); - if (!haveKeyserverConfigured()) { - result << QStringLiteral("--keyserver") << QStringLiteral("keys.gnupg.net"); - } result << QStringLiteral("--refresh-keys"); return result; } QString RefreshOpenPGPCertsCommand::errorCaption() const { return i18nc("@title:window", "OpenPGP Certificate Refresh Error"); } QString RefreshOpenPGPCertsCommand::successCaption() const { return i18nc("@title:window", "OpenPGP Certificate Refresh Finished"); } QString RefreshOpenPGPCertsCommand::crashExitMessage(const QStringList &args) const { return xi18nc("@info", "The GPG process that tried to refresh OpenPGP certificates " "ended prematurely because of an unexpected error." "Please check the output of %1 for details.", args.join(QLatin1Char(' '))); } QString RefreshOpenPGPCertsCommand::errorExitMessage(const QStringList &args) const { return xi18nc("@info", "An error occurred while trying to refresh OpenPGP certificates. " "The output from %1 was: %2", args[0], errorString()); } QString RefreshOpenPGPCertsCommand::successMessage(const QStringList &) const { return i18nc("@info", "OpenPGP certificates refreshed successfully."); // ### --check-trustdb } #include "moc_refreshopenpgpcertscommand.cpp" diff --git a/src/conf/dirservconfigpage.cpp b/src/conf/dirservconfigpage.cpp index be2f017a0..561599687 100644 --- a/src/conf/dirservconfigpage.cpp +++ b/src/conf/dirservconfigpage.cpp @@ -1,465 +1,476 @@ /* -*- mode: c++; c-basic-offset:4 -*- conf/dirservconfigpage.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2004, 2008 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 "dirservconfigpage.h" #include "labelledwidget.h" #include #include #include +#include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace QGpgME; // Option for configuring X.509 servers (available via gpgconf since GnuPG 2.3.5 and 2.2.34) static const char s_x509services_componentName[] = "dirmngr"; static const char s_x509services_entryName[] = "ldapserver"; // Legacy option for configuring X.509 servers (deprecated with GnuPG 2.2.28 and 2.3.2) static const char s_x509services_legacy_componentName[] = "gpgsm"; static const char s_x509services_legacy_entryName[] = "keyserver"; static const char s_pgpservice_componentName[] = "dirmngr"; static const char s_pgpservice_entryName[] = "keyserver"; // legacy config entry used until GnuPG 2.2 static const char s_pgpservice_legacy_componentName[] = "gpg"; static const char s_pgpservice_legacy_entryName[] = "keyserver"; static const char s_timeout_componentName[] = "dirmngr"; static const char s_timeout_entryName[] = "ldaptimeout"; static const char s_maxitems_componentName[] = "dirmngr"; static const char s_maxitems_entryName[] = "max-replies"; class DirectoryServicesConfigurationPage::Private { DirectoryServicesConfigurationPage *q = nullptr; public: Private(DirectoryServicesConfigurationPage *q); void load(); void save(); void defaults(); private: enum EntryMultiplicity { SingleValue, ListValue, }; enum ShowError { DoNotShowError, DoShowError, }; void setX509ServerEntry(const std::vector &servers); void load(const Kleo::Settings &settings); QGpgME::CryptoConfigEntry *configEntry(const char *componentName, const char *entryName, QGpgME::CryptoConfigEntry::ArgType argType, EntryMultiplicity multiplicity, ShowError showError); Kleo::LabelledWidget mOpenPGPKeyserverEdit; Kleo::DirectoryServicesWidget *mDirectoryServices = nullptr; Kleo::LabelledWidget mTimeout; Kleo::LabelledWidget mMaxItems; QCheckBox *mFetchMissingSignerKeysCB = nullptr; QCheckBox *mQueryWKDsForAllUserIDsCB = nullptr; QGpgME::CryptoConfigEntry *mOpenPGPServiceEntry = nullptr; QGpgME::CryptoConfigEntry *mTimeoutConfigEntry = nullptr; QGpgME::CryptoConfigEntry *mMaxItemsConfigEntry = nullptr; QGpgME::CryptoConfig *mConfig = nullptr; }; DirectoryServicesConfigurationPage::Private::Private(DirectoryServicesConfigurationPage *q) { mConfig = QGpgME::cryptoConfig(); auto glay = new QGridLayout(q->widget()); // OpenPGP keyserver int row = 0; { auto l = new QHBoxLayout{}; l->setContentsMargins(0, 0, 0, 0); mOpenPGPKeyserverEdit.createWidgets(q->widget()); mOpenPGPKeyserverEdit.label()->setText(i18n("OpenPGP keyserver:")); + if (engineIsVersion(2, 4, 4) // + || (engineIsVersion(2, 2, 42) && !engineIsVersion(2, 3, 0))) { + mOpenPGPKeyserverEdit.widget()->setToolTip( // + xi18nc("@info:tooltip", + "Enter the address of the keyserver to use when searching for OpenPGP certificates and " + "when uploading OpenPGP certificates. If you do not enter an address then an internal " + "default will be used. To disable the use of an OpenPGP keyserver enter the special value none.")); + } l->addWidget(mOpenPGPKeyserverEdit.label()); l->addWidget(mOpenPGPKeyserverEdit.widget()); glay->addLayout(l, row, 0, 1, 3); connect(mOpenPGPKeyserverEdit.widget(), &QLineEdit::textEdited, q, &DirectoryServicesConfigurationPage::markAsChanged); } // X.509 servers if (Settings{}.cmsEnabled()) { ++row; auto groupBox = new QGroupBox{i18n("X.509 Directory Services"), q->widget()}; groupBox->setFlat(true); auto groupBoxLayout = new QVBoxLayout{groupBox}; groupBoxLayout->setContentsMargins({}); if (gpgme_check_version("1.16.0")) { mDirectoryServices = new Kleo::DirectoryServicesWidget(q->widget()); if (QLayout *l = mDirectoryServices->layout()) { l->setContentsMargins(0, 0, 0, 0); } groupBoxLayout->addWidget(mDirectoryServices); connect(mDirectoryServices, &DirectoryServicesWidget::changed, q, &DirectoryServicesConfigurationPage::markAsChanged); } else { // QGpgME does not properly support keyserver flags for X.509 keyservers (added in GnuPG 2.2.28); // disable the configuration to prevent the configuration from being corrupted groupBoxLayout->addWidget(new QLabel{i18n("Configuration of directory services is not possible " "because the used gpgme libraries are too old."), q->widget()}); } glay->addWidget(groupBox, row, 0, 1, 3); } // LDAP timeout ++row; mTimeout.createWidgets(q->widget()); mTimeout.label()->setText(i18n("LDAP &timeout (minutes:seconds):")); mTimeout.widget()->setDisplayFormat(QStringLiteral("mm:ss")); connect(mTimeout.widget(), &QTimeEdit::timeChanged, q, &DirectoryServicesConfigurationPage::markAsChanged); glay->addWidget(mTimeout.label(), row, 0); glay->addWidget(mTimeout.widget(), row, 1); // Max number of items returned by queries ++row; mMaxItems.createWidgets(q->widget()); mMaxItems.label()->setText(i18n("&Maximum number of items returned by query:")); mMaxItems.widget()->setMinimum(0); connect(mMaxItems.widget(), &QSpinBox::valueChanged, q, &DirectoryServicesConfigurationPage::markAsChanged); glay->addWidget(mMaxItems.label(), row, 0); glay->addWidget(mMaxItems.widget(), row, 1); ++row; mFetchMissingSignerKeysCB = new QCheckBox{q->widget()}; mFetchMissingSignerKeysCB->setText(i18nc("@option:check", "Retrieve missing certification keys when importing new keys")); mFetchMissingSignerKeysCB->setToolTip(xi18nc("@info:tooltip", "If enabled, then Kleopatra will automatically try to retrieve the keys " "that were used to certify the user IDs of newly imported OpenPGP keys.")); connect(mFetchMissingSignerKeysCB, &QCheckBox::toggled, q, &DirectoryServicesConfigurationPage::markAsChanged); glay->addWidget(mFetchMissingSignerKeysCB, row, 0, 1, 3); ++row; mQueryWKDsForAllUserIDsCB = new QCheckBox{q->widget()}; mQueryWKDsForAllUserIDsCB->setText(i18nc("@option:check", "Query certificate directories of providers for all user IDs")); mQueryWKDsForAllUserIDsCB->setToolTip(xi18nc("@info:tooltip", "By default, Kleopatra only queries the certificate directories of providers (WKD) " "for user IDs that were originally retrieved from a WKD when you update an OpenPGP " "certificate. If this option is enabled, then Kleopatra will query WKDs for all user IDs.")); connect(mQueryWKDsForAllUserIDsCB, &QCheckBox::toggled, q, &DirectoryServicesConfigurationPage::markAsChanged); glay->addWidget(mQueryWKDsForAllUserIDsCB, row, 0, 1, 3); glay->setRowStretch(++row, 1); glay->setColumnStretch(2, 1); } static auto readKeyserverConfigs(const CryptoConfigEntry *configEntry) { std::vector servers; if (configEntry) { const auto urls = configEntry->urlValueList(); servers.reserve(urls.size()); std::transform(std::begin(urls), std::end(urls), std::back_inserter(servers), &KeyserverConfig::fromUrl); } return servers; } void DirectoryServicesConfigurationPage::Private::load(const Kleo::Settings &settings) { if (mDirectoryServices) { mDirectoryServices->clear(); // gpgsm uses the deprecated keyserver option in gpgsm.conf additionally to the ldapserver option in dirmngr.conf; // we (try to) read servers from both entries, but always write to the newest existing entry const auto *const newEntry = configEntry(s_x509services_componentName, s_x509services_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError); const auto *const legacyEntry = configEntry(s_x509services_legacy_componentName, s_x509services_legacy_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError); auto entry = newEntry ? newEntry : legacyEntry; if (entry) { const auto additionalServers = readKeyserverConfigs(legacyEntry); auto servers = readKeyserverConfigs(newEntry); std::copy(std::begin(additionalServers), std::end(additionalServers), std::back_inserter(servers)); mDirectoryServices->setKeyservers(servers); mDirectoryServices->setReadOnly(entry->isReadOnly()); } else { qCWarning(KLEOPATRA_LOG) << "Unknown or wrong typed config entries" << s_x509services_componentName << "/" << s_x509services_entryName << "and" << s_x509services_legacy_componentName << "/" << s_x509services_legacy_entryName; mDirectoryServices->setDisabled(true); } } { // gpg prefers the deprecated keyserver option in gpg.conf over the keyserver option in dirmngr.conf; // therefore, we use the deprecated keyserver option if it is set or if the new option doesn't exist (gpg < 2.1.9) auto const newEntry = configEntry(s_pgpservice_componentName, s_pgpservice_entryName, CryptoConfigEntry::ArgType_String, SingleValue, DoNotShowError); auto const legacyEntry = configEntry(s_pgpservice_legacy_componentName, s_pgpservice_legacy_entryName, CryptoConfigEntry::ArgType_String, SingleValue, DoNotShowError); mOpenPGPServiceEntry = ((legacyEntry && legacyEntry->isSet()) || !newEntry) ? legacyEntry : newEntry; if (!mOpenPGPServiceEntry) { qCWarning(KLEOPATRA_LOG) << "Unknown or wrong typed config entries" << s_pgpservice_componentName << "/" << s_pgpservice_entryName << "and" << s_pgpservice_legacy_componentName << "/" << s_pgpservice_legacy_entryName; } else if (mOpenPGPServiceEntry == legacyEntry) { qCDebug(KLEOPATRA_LOG) << "Using config entry" << s_pgpservice_legacy_componentName << "/" << s_pgpservice_legacy_entryName; } else { qCDebug(KLEOPATRA_LOG) << "Using config entry" << s_pgpservice_componentName << "/" << s_pgpservice_entryName; } mOpenPGPKeyserverEdit.widget()->setText(mOpenPGPServiceEntry && mOpenPGPServiceEntry->isSet() ? mOpenPGPServiceEntry->stringValue() : QString()); mOpenPGPKeyserverEdit.setEnabled(mOpenPGPServiceEntry && !mOpenPGPServiceEntry->isReadOnly()); if (newEntry && !newEntry->defaultValue().isNull()) { mOpenPGPKeyserverEdit.widget()->setPlaceholderText(newEntry->defaultValue().toString()); } else { if (GpgME::engineInfo(GpgME::GpgEngine).engineVersion() < "2.1.16") { mOpenPGPKeyserverEdit.widget()->setPlaceholderText(QStringLiteral("hkp://keys.gnupg.net")); } else { mOpenPGPKeyserverEdit.widget()->setPlaceholderText(QStringLiteral("hkps://hkps.pool.sks-keyservers.net")); } } } // read LDAP timeout // first try to read the config entry as int (GnuPG 2.3) mTimeoutConfigEntry = configEntry(s_timeout_componentName, s_timeout_entryName, CryptoConfigEntry::ArgType_Int, SingleValue, DoNotShowError); if (!mTimeoutConfigEntry) { // if this fails, then try to read the config entry as unsigned int (GnuPG <= 2.2) mTimeoutConfigEntry = configEntry(s_timeout_componentName, s_timeout_entryName, CryptoConfigEntry::ArgType_UInt, SingleValue, DoShowError); } if (mTimeoutConfigEntry) { const int ldapTimeout = mTimeoutConfigEntry->argType() == CryptoConfigEntry::ArgType_Int ? mTimeoutConfigEntry->intValue() : static_cast(mTimeoutConfigEntry->uintValue()); const QTime time = QTime(0, 0, 0, 0).addSecs(ldapTimeout); // qCDebug(KLEOPATRA_LOG) <<"timeout:" << mTimeoutConfigEntry->uintValue() <<" ->" << time; mTimeout.widget()->setTime(time); } mTimeout.setEnabled(mTimeoutConfigEntry && !mTimeoutConfigEntry->isReadOnly()); // read max-replies config entry // first try to read the config entry as int (GnuPG 2.3) mMaxItemsConfigEntry = configEntry(s_maxitems_componentName, s_maxitems_entryName, CryptoConfigEntry::ArgType_Int, SingleValue, DoNotShowError); if (!mMaxItemsConfigEntry) { // if this fails, then try to read the config entry as unsigned int (GnuPG <= 2.2) mMaxItemsConfigEntry = configEntry(s_maxitems_componentName, s_maxitems_entryName, CryptoConfigEntry::ArgType_UInt, SingleValue, DoShowError); } if (mMaxItemsConfigEntry) { const int value = mMaxItemsConfigEntry->argType() == CryptoConfigEntry::ArgType_Int ? mMaxItemsConfigEntry->intValue() : static_cast(mMaxItemsConfigEntry->uintValue()); mMaxItems.widget()->blockSignals(true); // KNumInput emits valueChanged from setValue! mMaxItems.widget()->setValue(value); mMaxItems.widget()->blockSignals(false); } mMaxItems.setEnabled(mMaxItemsConfigEntry && !mMaxItemsConfigEntry->isReadOnly()); mFetchMissingSignerKeysCB->setChecked(settings.retrieveSignerKeysAfterImport()); mFetchMissingSignerKeysCB->setEnabled(!settings.isImmutable(QStringLiteral("RetrieveSignerKeysAfterImport"))); mQueryWKDsForAllUserIDsCB->setChecked(settings.queryWKDsForAllUserIDs()); mQueryWKDsForAllUserIDsCB->setEnabled(!settings.isImmutable(QStringLiteral("QueryWKDsForAllUserIDs"))); } void DirectoryServicesConfigurationPage::Private::load() { load(Settings{}); } namespace { void updateIntegerConfigEntry(QGpgME::CryptoConfigEntry *configEntry, int value) { if (!configEntry) { return; } if (configEntry->argType() == CryptoConfigEntry::ArgType_Int) { if (configEntry->intValue() != value) { configEntry->setIntValue(value); } } else { const auto newValue = static_cast(value); if (configEntry->uintValue() != newValue) { configEntry->setUIntValue(newValue); } } } } void DirectoryServicesConfigurationPage::Private::setX509ServerEntry(const std::vector &servers) { const auto newEntry = configEntry(s_x509services_componentName, s_x509services_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError); const auto legacyEntry = configEntry(s_x509services_legacy_componentName, s_x509services_legacy_entryName, CryptoConfigEntry::ArgType_LDAPURL, ListValue, DoNotShowError); if ((newEntry && newEntry->isReadOnly()) || (legacyEntry && legacyEntry->isReadOnly())) { // do not change the config entries if either config entry is read-only return; } QList urls; urls.reserve(servers.size()); std::transform(std::begin(servers), std::end(servers), std::back_inserter(urls), std::mem_fn(&KeyserverConfig::toUrl)); if (newEntry) { // write all servers to the new config entry newEntry->setURLValueList(urls); // and clear the legacy config entry if (legacyEntry) { legacyEntry->setURLValueList({}); } } else if (legacyEntry) { // write all servers to the legacy config entry if the new entry is not available legacyEntry->setURLValueList(urls); } else { qCWarning(KLEOPATRA_LOG) << "Could not store the X.509 servers. Unknown or wrong typed config entries" << s_x509services_componentName << "/" << s_x509services_entryName << "and" << s_x509services_legacy_componentName << "/" << s_x509services_legacy_entryName; } } void DirectoryServicesConfigurationPage::Private::save() { if (mDirectoryServices && mDirectoryServices->isEnabled()) { setX509ServerEntry(mDirectoryServices->keyservers()); } if (mOpenPGPServiceEntry) { const auto keyserver = mOpenPGPKeyserverEdit.widget()->text().trimmed(); if (keyserver.isEmpty()) { mOpenPGPServiceEntry->resetToDefault(); + } else if (keyserver == QLatin1String{"none"}) { + mOpenPGPServiceEntry->setStringValue(keyserver); } else { const auto keyserverUrl = keyserver.contains(QLatin1String{"://"}) ? keyserver : (QLatin1String{"hkps://"} + keyserver); mOpenPGPServiceEntry->setStringValue(keyserverUrl); } } const QTime time{mTimeout.widget()->time()}; updateIntegerConfigEntry(mTimeoutConfigEntry, time.minute() * 60 + time.second()); updateIntegerConfigEntry(mMaxItemsConfigEntry, mMaxItems.widget()->value()); mConfig->sync(true); Settings settings; settings.setRetrieveSignerKeysAfterImport(mFetchMissingSignerKeysCB->isChecked()); settings.setQueryWKDsForAllUserIDs(mQueryWKDsForAllUserIDsCB->isChecked()); settings.save(); } void DirectoryServicesConfigurationPage::Private::defaults() { // these guys don't have a default, to clear them: if (mDirectoryServices && mDirectoryServices->isEnabled()) { setX509ServerEntry({}); } if (mOpenPGPServiceEntry && !mOpenPGPServiceEntry->isReadOnly()) { mOpenPGPServiceEntry->setStringValue(QString()); } // these presumably have a default, use that one: if (mTimeoutConfigEntry && !mTimeoutConfigEntry->isReadOnly()) { mTimeoutConfigEntry->resetToDefault(); } if (mMaxItemsConfigEntry && !mMaxItemsConfigEntry->isReadOnly()) { mMaxItemsConfigEntry->resetToDefault(); } Settings settings; settings.setRetrieveSignerKeysAfterImport(settings.findItem(QStringLiteral("RetrieveSignerKeysAfterImport"))->getDefault().toBool()); settings.setQueryWKDsForAllUserIDs(settings.findItem(QStringLiteral("QueryWKDsForAllUserIDs"))->getDefault().toBool()); load(settings); } // Find config entry for ldap servers. Implements runtime checks on the configuration option. CryptoConfigEntry *DirectoryServicesConfigurationPage::Private::configEntry(const char *componentName, const char *entryName, CryptoConfigEntry::ArgType argType, EntryMultiplicity multiplicity, ShowError showError) { CryptoConfigEntry *const entry = Kleo::getCryptoConfigEntry(mConfig, componentName, entryName); if (!entry) { if (showError == DoShowError) { KMessageBox::error( q->widget(), i18n("Backend error: gpgconf does not seem to know the entry for %1/%2", QLatin1String(componentName), QLatin1String(entryName))); } return nullptr; } if (entry->argType() != argType || entry->isList() != bool(multiplicity)) { if (showError == DoShowError) { KMessageBox::error(q->widget(), i18n("Backend error: gpgconf has wrong type for %1/%2: %3 %4", QLatin1String(componentName), QLatin1String(entryName), entry->argType(), entry->isList())); } return nullptr; } return entry; } DirectoryServicesConfigurationPage::DirectoryServicesConfigurationPage(QObject *parent, const KPluginMetaData &data) : KCModule(parent, data) , d{new Private{this}} { } DirectoryServicesConfigurationPage::~DirectoryServicesConfigurationPage() = default; void DirectoryServicesConfigurationPage::load() { d->load(); } void DirectoryServicesConfigurationPage::save() { d->save(); } void DirectoryServicesConfigurationPage::defaults() { d->defaults(); } #include "moc_dirservconfigpage.cpp" diff --git a/src/conf/groupsconfigdialog.cpp b/src/conf/groupsconfigdialog.cpp index 2dba206ba..03658d8fd 100644 --- a/src/conf/groupsconfigdialog.cpp +++ b/src/conf/groupsconfigdialog.cpp @@ -1,168 +1,168 @@ /* - conf/groupsconfigdialog.h + conf/groupsconfigdialog.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 "groupsconfigdialog.h" #include "groupsconfigpage.h" #include "utils/gui-helper.h" #include #include #include #include #include #include #include #include #include #include class GroupsConfigDialog::Private { friend class ::GroupsConfigDialog; GroupsConfigDialog *const q; GroupsConfigPage *configPage = nullptr; bool initialFocusWasSet = false; public: Private(GroupsConfigDialog *qq) : q(qq) , configPage(new GroupsConfigPage(qq)) { restoreLayout(); } ~Private() { saveLayout(); } void setInitialFocus() { if (initialFocusWasSet) { return; } // this is a bit hacky, but fixing the focus chain where the dialog // button box comes before the page, which causes the first button in // the button box to be focussed initially, is even more hacky Q_ASSERT(configPage->findChildren().size() == 1); if (auto searchField = configPage->findChild()) { searchField->setFocus(); } initialFocusWasSet = true; } private: void saveLayout() { KConfigGroup configGroup(KSharedConfig::openStateConfig(), QStringLiteral("GroupsConfigDialog")); configGroup.writeEntry("Size", q->size()); configGroup.sync(); } void restoreLayout(const QSize &defaultSize = QSize()) { const KConfigGroup configGroup(KSharedConfig::openStateConfig(), QStringLiteral("GroupsConfigDialog")); const QSize size = configGroup.readEntry("Size", defaultSize); if (size.isValid()) { q->resize(size); } } }; GroupsConfigDialog::GroupsConfigDialog(QWidget *parent) : KConfigDialog(parent, GroupsConfigDialog::dialogName(), /*config=*/nullptr) , d(new Private(this)) { setWindowTitle(i18nc("@title:window", "Configure Groups")); setFaceType(KPageDialog::Plain); const auto *const item = addPage(d->configPage, i18n("Groups"), /*pixmapName=*/QString(), /*header=*/QString(), /*manage=*/false); // prevent scroll area embedding the config page from receiving focus const auto scrollAreas = item->widget()->findChildren(); for (auto sa : scrollAreas) { sa->setFocusPolicy(Qt::NoFocus); } // there are no defaults to restore buttonBox()->removeButton(buttonBox()->button(QDialogButtonBox::RestoreDefaults)); QPushButton *resetButton = buttonBox()->addButton(QDialogButtonBox::Reset); KGuiItem::assign(resetButton, KStandardGuiItem::reset()); resetButton->setEnabled(false); const auto helpAction = new Kleo::DocAction(QIcon::fromTheme(QStringLiteral("help")), i18n("Help"), i18nc("Only available in German and English. Leave to English for other languages.", "handout_group-feature_gnupg_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd")); if (helpAction->isEnabled()) { auto helpButton = buttonBox()->button(QDialogButtonBox::Help); if (helpButton) { disconnect(helpButton, &QAbstractButton::clicked, nullptr, nullptr); connect(helpButton, &QAbstractButton::clicked, helpAction, &QAction::trigger); connect(helpButton, &QObject::destroyed, helpAction, &QObject::deleteLater); } } else { delete helpAction; } // prevent accidental closing of dialog when pressing Enter while the search field has focus Kleo::unsetAutoDefaultButtons(this); connect(buttonBox()->button(QDialogButtonBox::Reset), &QAbstractButton::clicked, this, &GroupsConfigDialog::updateWidgets); connect(d->configPage, &GroupsConfigPage::changed, this, [this]() { updateButtons(); if (QPushButton *button = buttonBox()->button(QDialogButtonBox::Reset)) { button->setEnabled(d->configPage->hasChanged()); } }); } GroupsConfigDialog::~GroupsConfigDialog() = default; QString GroupsConfigDialog::dialogName() { return QStringLiteral("Group Settings"); } void GroupsConfigDialog::showEvent(QShowEvent *event) { d->setInitialFocus(); KConfigDialog::showEvent(event); // prevent accidental closing of dialog when pressing Enter while the search field has focus Kleo::unsetDefaultButtons(buttonBox()); } void GroupsConfigDialog::updateSettings() { d->configPage->save(); } void GroupsConfigDialog::updateWidgets() { d->configPage->load(); } bool GroupsConfigDialog::hasChanged() { return d->configPage->hasChanged(); } #include "moc_groupsconfigdialog.cpp" diff --git a/src/crypto/autodecryptverifyfilescontroller.cpp b/src/crypto/autodecryptverifyfilescontroller.cpp index bbaaa59d9..5e2f62c09 100644 --- a/src/crypto/autodecryptverifyfilescontroller.cpp +++ b/src/crypto/autodecryptverifyfilescontroller.cpp @@ -1,615 +1,664 @@ /* -*- mode: c++; c-basic-offset:4 -*- autodecryptverifyfilescontroller.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "autodecryptverifyfilescontroller.h" #include "fileoperationspreferences.h" #include #include #include #include #include "commands/decryptverifyfilescommand.h" #include #include #include #include #include #include #include #ifndef Q_OS_WIN #include #include #endif #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include using namespace GpgME; using namespace Kleo; using namespace Kleo::Crypto; using namespace Kleo::Crypto::Gui; class AutoDecryptVerifyFilesController::Private { AutoDecryptVerifyFilesController *const q; public: explicit Private(AutoDecryptVerifyFilesController *qq); void schedule(); QString getEmbeddedFileName(const QString &fileName) const; void exec(); std::vector> buildTasks(const QStringList &, QStringList &); struct CryptoFile { QString baseName; QString fileName; GpgME::Protocol protocol = GpgME::UnknownProtocol; int classification = 0; std::shared_ptr output; }; QList classifyAndSortFiles(const QStringList &files); void reportError(int err, const QString &details) { q->setLastError(err, details); q->emitDoneOrError(); } void cancelAllTasks(); QStringList m_passedFiles, m_filesAfterPreparation; std::vector> m_results; std::vector> m_runnableTasks, m_completedTasks; std::shared_ptr m_runningTask; bool m_errorDetected = false; DecryptVerifyOperation m_operation = DecryptVerify; QPointer m_dialog; std::unique_ptr m_workDir; }; AutoDecryptVerifyFilesController::Private::Private(AutoDecryptVerifyFilesController *qq) : q(qq) { qRegisterMetaType(); } void AutoDecryptVerifyFilesController::Private::schedule() { if (!m_runningTask && !m_runnableTasks.empty()) { const std::shared_ptr t = m_runnableTasks.back(); m_runnableTasks.pop_back(); t->start(); m_runningTask = t; } if (!m_runningTask) { kleo_assert(m_runnableTasks.empty()); for (const std::shared_ptr &i : std::as_const(m_results)) { Q_EMIT q->verificationResult(i->verificationResult()); } } } QString AutoDecryptVerifyFilesController::Private::getEmbeddedFileName(const QString &fileName) const { auto it = std::find_if(m_results.cbegin(), m_results.cend(), [fileName](const auto &r) { return r->fileName() == fileName; }); if (it != m_results.cend()) { const auto embeddedFilePath = QString::fromUtf8((*it)->decryptionResult().fileName()); if (embeddedFilePath.contains(QLatin1Char{'\\'})) { // ignore embedded file names containing '\' return {}; } // strip the path from the embedded file name return QFileInfo{embeddedFilePath}.fileName(); } else { return {}; } } void AutoDecryptVerifyFilesController::Private::exec() { Q_ASSERT(!m_dialog); QStringList undetected; std::vector> tasks = buildTasks(m_passedFiles, undetected); if (!undetected.isEmpty()) { // Since GpgME 1.7.0 Classification is supposed to be reliable // so we really can't do anything with this data. reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to find encrypted or signed data in one or more files." "You can manually select what to do with the files now." "If they contain signed or encrypted data please report a bug (see Help->Report Bug).")); auto cmd = new Commands::DecryptVerifyFilesCommand(undetected, nullptr, true); cmd->start(); } if (tasks.empty()) { q->emitDoneOrError(); return; } Q_ASSERT(m_runnableTasks.empty()); m_runnableTasks.swap(tasks); std::shared_ptr coll(new TaskCollection); for (const std::shared_ptr &i : std::as_const(m_runnableTasks)) { q->connectTask(i); } coll->setTasks(m_runnableTasks); DecryptVerifyFilesDialog dialog{coll}; m_dialog = &dialog; m_dialog->setOutputLocation(heuristicBaseDirectory(m_passedFiles)); QTimer::singleShot(0, q, SLOT(schedule())); const auto result = m_dialog->exec(); if (result == QDialog::Rejected) { q->cancel(); } else if (result == QDialog::Accepted && m_workDir) { // Without workdir there is nothing to move. const QDir workdir(m_workDir->path()); const QDir outDir(m_dialog->outputLocation()); bool overWriteAll = false; qCDebug(KLEOPATRA_LOG) << workdir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); for (const QFileInfo &fi : workdir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot)) { const auto inpath = fi.absoluteFilePath(); if (fi.isDir()) { // A directory. Assume that the input was an archive // and avoid directory merges by trying to find a non // existing directory. auto candidate = fi.fileName(); if (candidate.startsWith(QLatin1Char('-'))) { // Bug in GpgTar Extracts stdout passed archives to a dir named - candidate = QFileInfo(m_passedFiles.first()).baseName(); } QString suffix; QFileInfo ofi; int i = 0; do { ofi = QFileInfo(outDir.absoluteFilePath(candidate + suffix)); if (!ofi.exists()) { break; } suffix = QStringLiteral("_%1").arg(++i); } while (i < 1000); const auto destPath = ofi.absoluteFilePath(); #ifndef Q_OS_WIN auto job = KIO::moveAs(QUrl::fromLocalFile(inpath), QUrl::fromLocalFile(destPath)); qCDebug(KLEOPATRA_LOG) << "Moving" << job->srcUrls().front().toLocalFile() << "to" << job->destUrl().toLocalFile(); if (!job->exec()) { if (job->error() == KIO::ERR_USER_CANCELED) { break; } reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18nc("@info", "Failed to move %1 to %2." "%3", inpath, destPath, job->errorString())); } #else // On Windows, KIO::move does not work for folders when crossing partition boundaries if (!moveDir(inpath, destPath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18nc("@info", "Failed to move %1 to %2.", inpath, destPath)); } #endif continue; } const auto embeddedFileName = getEmbeddedFileName(inpath); QString outFileName = fi.fileName(); if (!embeddedFileName.isEmpty() && embeddedFileName != fi.fileName()) { // we switch "Yes" and "No" because Yes is default, but saving with embedded file name could be dangerous const auto answer = KMessageBox::questionTwoActionsCancel( m_dialog, xi18n("Shall the file be saved with the original file name %1?", embeddedFileName), i18nc("@title:window", "Use Original File Name?"), KGuiItem(xi18n("No, Save As %1", fi.fileName())), KGuiItem(xi18n("Yes, Save As %1", embeddedFileName))); if (answer == KMessageBox::Cancel) { qCDebug(KLEOPATRA_LOG) << "Saving canceled for:" << inpath; continue; } else if (answer == KMessageBox::ButtonCode::SecondaryAction) { outFileName = embeddedFileName; } } const auto outpath = outDir.absoluteFilePath(outFileName); qCDebug(KLEOPATRA_LOG) << "Moving " << inpath << " to " << outpath; const QFileInfo ofi(outpath); if (ofi.exists()) { int sel = KMessageBox::Cancel; if (!overWriteAll) { sel = KMessageBox::questionTwoActionsCancel(m_dialog, i18n("The file %1 already exists.\n" "Overwrite?", outpath), i18n("Overwrite Existing File?"), KStandardGuiItem::overwrite(), KGuiItem(i18n("Overwrite All")), KStandardGuiItem::cancel()); } if (sel == KMessageBox::Cancel) { qCDebug(KLEOPATRA_LOG) << "Overwriting canceled for: " << outpath; continue; } if (sel == KMessageBox::ButtonCode::SecondaryAction) { // Overwrite All overWriteAll = true; } if (!QFile::remove(outpath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to delete %1.", outpath)); continue; } } if (!QFile::rename(inpath, outpath)) { reportError(makeGnuPGError(GPG_ERR_GENERAL), xi18n("Failed to move %1 to %2.", inpath, outpath)); } } } q->emitDoneOrError(); } QList AutoDecryptVerifyFilesController::Private::classifyAndSortFiles(const QStringList &files) { const auto isSignature = [](int classification) -> bool { return mayBeDetachedSignature(classification) // || mayBeOpaqueSignature(classification) // || (classification & Class::TypeMask) == Class::ClearsignedMessage; }; QList out; for (const auto &file : files) { CryptoFile cFile; cFile.fileName = file; cFile.baseName = stripSuffix(file); cFile.classification = classify(file); cFile.protocol = findProtocol(cFile.classification); auto it = std::find_if(out.begin(), out.end(), [&cFile](const CryptoFile &other) { return other.protocol == cFile.protocol && other.baseName == cFile.baseName; }); if (it != out.end()) { // If we found a file with the same basename, make sure that encrypted // file is before the signature file, so that we first decrypt and then // verify if (isSignature(cFile.classification) && isCipherText(it->classification)) { out.insert(it + 1, cFile); } else if (isCipherText(cFile.classification) && isSignature(it->classification)) { out.insert(it, cFile); } else { // both are signatures or both are encrypted files, in which // case order does not matter out.insert(it, cFile); } } else { out.push_back(cFile); } } return out; } static bool archiveJobsCanBeUsed([[maybe_unused]] GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::DecryptVerifyArchiveJob::isSupported(); } std::vector> AutoDecryptVerifyFilesController::Private::buildTasks(const QStringList &fileNames, QStringList &undetected) { // sort files so that we make sure we first decrypt and then verify QList cryptoFiles = classifyAndSortFiles(fileNames); std::vector> tasks; for (auto it = cryptoFiles.begin(), end = cryptoFiles.end(); it != end; ++it) { auto &cFile = (*it); QFileInfo fi(cFile.fileName); qCDebug(KLEOPATRA_LOG) << "classified" << cFile.fileName << "as" << printableClassification(cFile.classification); if (!fi.isReadable()) { reportError(makeGnuPGError(GPG_ERR_ASS_NO_INPUT), xi18n("Cannot open %1 for reading.", cFile.fileName)); continue; } if (mayBeAnyCertStoreType(cFile.classification)) { // Trying to verify a certificate. Possible because extensions are often similar // for PGP Keys. reportError(makeGnuPGError(GPG_ERR_ASS_NO_INPUT), xi18n("The file %1 contains certificates and can't be decrypted or verified.", cFile.fileName)); qCDebug(KLEOPATRA_LOG) << "reported error"; continue; } // We can't reliably detect CMS detached signatures, so we will try to do // our best to use the current file as a detached signature and fallback to // opaque signature otherwise. if (cFile.protocol == GpgME::CMS && mayBeDetachedSignature(cFile.classification)) { // First, see if previous task was a decryption task for the same file // and "pipe" it's output into our input std::shared_ptr input; bool prepend = false; if (it != cryptoFiles.begin()) { const auto prev = it - 1; if (prev->protocol == cFile.protocol && prev->baseName == cFile.baseName) { input = Input::createFromOutput(prev->output); prepend = true; } } if (!input) { if (QFile::exists(cFile.baseName)) { input = Input::createFromFile(cFile.baseName); } } if (input) { qCDebug(KLEOPATRA_LOG) << "Detached CMS verify: " << cFile.fileName; std::shared_ptr t(new VerifyDetachedTask); t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(input); t->setProtocol(cFile.protocol); if (prepend) { // Put the verify task BEFORE the decrypt task in the tasks queue, // because the tasks are executed in reverse order! tasks.insert(tasks.end() - 1, t); } else { tasks.push_back(t); } continue; } else { // No signed data, maybe not a detached signature } } if (isDetachedSignature(cFile.classification)) { // Detached signature, try to find data or ask the user. QString signedDataFileName = cFile.baseName; if (!QFile::exists(signedDataFileName)) { signedDataFileName = QFileDialog::getOpenFileName(nullptr, xi18n("Select the file to verify with the signature %1", fi.fileName()), fi.path()); } if (signedDataFileName.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "No signed data selected. Verify aborted."; } else { qCDebug(KLEOPATRA_LOG) << "Detached verify: " << cFile.fileName << " Data: " << signedDataFileName; std::shared_ptr t(new VerifyDetachedTask); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (cFile.protocol == GpgME::OpenPGP) { + t->setSignatureFile(cFile.fileName); + t->setSignedFile(signedDataFileName); + } else { + t->setInput(Input::createFromFile(cFile.fileName)); + t->setSignedData(Input::createFromFile(signedDataFileName)); + } +#else t->setInput(Input::createFromFile(cFile.fileName)); t->setSignedData(Input::createFromFile(signedDataFileName)); +#endif t->setProtocol(cFile.protocol); tasks.push_back(t); } continue; } if (!mayBeAnyMessageType(cFile.classification)) { // Not a Message? Maybe there is a signature for this file? const auto signatures = findSignatures(cFile.fileName); bool foundSig = false; if (!signatures.empty()) { for (const QString &sig : signatures) { const auto classification = classify(sig); qCDebug(KLEOPATRA_LOG) << "Guessing: " << sig << " is a signature for: " << cFile.fileName << "Classification: " << classification; const auto proto = findProtocol(classification); if (proto == GpgME::UnknownProtocol) { qCDebug(KLEOPATRA_LOG) << "Could not determine protocol. Skipping guess."; continue; } foundSig = true; std::shared_ptr t(new VerifyDetachedTask); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (cFile.protocol == GpgME::OpenPGP) { + t->setSignatureFile(sig); + t->setSignedFile(cFile.fileName); + } else { + t->setInput(Input::createFromFile(sig)); + t->setSignedData(Input::createFromFile(cFile.fileName)); + } +#else t->setInput(Input::createFromFile(sig)); t->setSignedData(Input::createFromFile(cFile.fileName)); +#endif t->setProtocol(proto); tasks.push_back(t); } } if (!foundSig) { undetected << cFile.fileName; qCDebug(KLEOPATRA_LOG) << "Failed detection for: " << cFile.fileName << " adding to undetected."; } } else { const FileOperationsPreferences fileOpSettings; // Any Message type so we have input and output. - const auto input = Input::createFromFile(cFile.fileName); + std::shared_ptr input; +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (cFile.protocol != GpgME::OpenPGP) { + input = Input::createFromFile(cFile.fileName); + } +#else + input = Input::createFromFile(cFile.fileName); +#endif std::shared_ptr ad; if (fileOpSettings.autoExtractArchives()) { const auto archiveDefinitions = ArchiveDefinition::getArchiveDefinitions(); ad = q->pick_archive_definition(cFile.protocol, archiveDefinitions, cFile.fileName); } if (fileOpSettings.dontUseTmpDir()) { if (!m_workDir) { m_workDir = std::make_unique(heuristicBaseDirectory(fileNames) + QStringLiteral("/kleopatra-XXXXXX")); } if (!m_workDir->isValid()) { qCDebug(KLEOPATRA_LOG) << heuristicBaseDirectory(fileNames) << "not a valid temporary directory."; m_workDir.reset(); } } if (!m_workDir) { m_workDir = std::make_unique(); } qCDebug(KLEOPATRA_LOG) << "Using:" << m_workDir->path() << "as temporary directory."; const auto wd = QDir(m_workDir->path()); std::shared_ptr output; + QString outputFilePath; if (ad) { if ((ad->id() == QLatin1String{"tar"}) && archiveJobsCanBeUsed(cFile.protocol)) { // we don't need an output } else { output = ad->createOutputFromUnpackCommand(cFile.protocol, ad->stripExtension(cFile.protocol, cFile.baseName), wd); } } else { - output = Output::createFromFile(wd.absoluteFilePath(outputFileName(fi.fileName())), false); + outputFilePath = wd.absoluteFilePath(outputFileName(fi.fileName())); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (cFile.protocol != GpgME::OpenPGP) { + output = Output::createFromFile(outputFilePath, false); + } +#else + output = Output::createFromFile(outputFilePath, false); +#endif } // If this might be opaque CMS signature, then try that. We already handled // detached CMS signature above const auto isCMSOpaqueSignature = cFile.protocol == GpgME::CMS && mayBeOpaqueSignature(cFile.classification); if (isOpaqueSignature(cFile.classification) || isCMSOpaqueSignature) { qCDebug(KLEOPATRA_LOG) << "creating a VerifyOpaqueTask"; std::shared_ptr t(new VerifyOpaqueTask); - t->setInput(input); + if (input) { + t->setInput(input); + } if (output) { t->setOutput(output); } t->setProtocol(cFile.protocol); if (ad) { t->setExtractArchive(true); t->setInputFile(cFile.fileName); if (output) { t->setOutputDirectory(m_workDir->path()); } else { // make gpgtar extract to a subfolder of the work directory based on the input file name const auto baseFileName = QFileInfo{ad->stripExtension(cFile.protocol, cFile.baseName)}.fileName(); t->setOutputDirectory(QDir{m_workDir->path()}.filePath(baseFileName)); } +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + } else if (cFile.protocol == GpgME::OpenPGP) { + t->setInputFile(cFile.fileName); + t->setOutputFile(outputFilePath); +#endif } tasks.push_back(t); } else { // Any message. That is not an opaque signature needs to be // decrypted. Verify we always do because we can't know if // an encrypted message is also signed. qCDebug(KLEOPATRA_LOG) << "creating a DecryptVerifyTask"; std::shared_ptr t(new DecryptVerifyTask); - t->setInput(input); + if (input) { + t->setInput(input); + } if (output) { t->setOutput(output); } t->setProtocol(cFile.protocol); if (ad) { t->setExtractArchive(true); t->setInputFile(cFile.fileName); if (output) { t->setOutputDirectory(m_workDir->path()); } else { // make gpgtar extract to a subfolder of the work directory based on the input file name const auto baseFileName = QFileInfo{ad->stripExtension(cFile.protocol, cFile.baseName)}.fileName(); t->setOutputDirectory(QDir{m_workDir->path()}.filePath(baseFileName)); } +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + } else if (cFile.protocol == GpgME::OpenPGP) { + t->setInputFile(cFile.fileName); + t->setOutputFile(outputFilePath); +#endif } cFile.output = output; tasks.push_back(t); } } } return tasks; } void AutoDecryptVerifyFilesController::setFiles(const QStringList &files) { d->m_passedFiles = files; } AutoDecryptVerifyFilesController::AutoDecryptVerifyFilesController(QObject *parent) : DecryptVerifyFilesController(parent) , d(new Private(this)) { } AutoDecryptVerifyFilesController::AutoDecryptVerifyFilesController(const std::shared_ptr &ctx, QObject *parent) : DecryptVerifyFilesController(ctx, parent) , d(new Private(this)) { } AutoDecryptVerifyFilesController::~AutoDecryptVerifyFilesController() { qCDebug(KLEOPATRA_LOG); } void AutoDecryptVerifyFilesController::start() { d->exec(); } void AutoDecryptVerifyFilesController::setOperation(DecryptVerifyOperation op) { d->m_operation = op; } DecryptVerifyOperation AutoDecryptVerifyFilesController::operation() const { return d->m_operation; } void AutoDecryptVerifyFilesController::Private::cancelAllTasks() { // we just kill all runnable tasks - this will not result in // signal emissions. m_runnableTasks.clear(); // a cancel() will result in a call to if (m_runningTask) { m_runningTask->cancel(); } } void AutoDecryptVerifyFilesController::cancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; try { d->m_errorDetected = true; if (d->m_dialog) { d->m_dialog->close(); } d->cancelAllTasks(); } catch (const std::exception &e) { qCDebug(KLEOPATRA_LOG) << "Caught exception: " << e.what(); } } void AutoDecryptVerifyFilesController::doTaskDone(const Task *task, const std::shared_ptr &result) { Q_ASSERT(task); Q_UNUSED(task) // We could just delete the tasks here, but we can't use // Qt::QueuedConnection here (we need sender()) and other slots // might not yet have executed. Therefore, we push completed tasks // into a burial container d->m_completedTasks.push_back(d->m_runningTask); d->m_runningTask.reset(); if (const std::shared_ptr &dvr = std::dynamic_pointer_cast(result)) { d->m_results.push_back(dvr); } QTimer::singleShot(0, this, SLOT(schedule())); } #include "moc_autodecryptverifyfilescontroller.cpp" diff --git a/src/crypto/decryptverifytask.cpp b/src/crypto/decryptverifytask.cpp index 881a2e16e..eb761c749 100644 --- a/src/crypto/decryptverifytask.cpp +++ b/src/crypto/decryptverifytask.cpp @@ -1,1821 +1,1926 @@ /* -*- mode: c++; c-basic-offset:4 -*- decryptverifytask.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 "decryptverifytask.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 "kleopatra_debug.h" #include #include #include #include #include +#include #include #include #include #include #include #include // Qt::escape #include #include using namespace Kleo::Crypto; using namespace Kleo; using namespace GpgME; using namespace KMime::Types; namespace { static AuditLogEntry auditLogFromSender(QObject *sender) { return AuditLogEntry::fromJob(qobject_cast(sender)); } static bool addrspec_equal(const AddrSpec &lhs, const AddrSpec &rhs, Qt::CaseSensitivity cs) { return lhs.localPart.compare(rhs.localPart, cs) == 0 && lhs.domain.compare(rhs.domain, Qt::CaseInsensitive) == 0; } static bool mailbox_equal(const Mailbox &lhs, const Mailbox &rhs, Qt::CaseSensitivity cs) { return addrspec_equal(lhs.addrSpec(), rhs.addrSpec(), cs); } static std::string stripAngleBrackets(const std::string &str) { if (str.empty()) { return str; } if (str[0] == '<' && str[str.size() - 1] == '>') { return str.substr(1, str.size() - 2); } return str; } static std::string email(const UserID &uid) { if (uid.parent().protocol() == OpenPGP) { if (const char *const email = uid.email()) { return stripAngleBrackets(email); } else { return std::string(); } } Q_ASSERT(uid.parent().protocol() == CMS); if (const char *const id = uid.id()) if (*id == '<') { return stripAngleBrackets(id); } else { return DN(id)[QStringLiteral("EMAIL")].trimmed().toUtf8().constData(); } else { return std::string(); } } static Mailbox mailbox(const UserID &uid) { const std::string e = email(uid); Mailbox mbox; if (!e.empty()) { mbox.setAddress(e.c_str()); } return mbox; } static std::vector extractMailboxes(const Key &key) { std::vector res; const auto userIDs{key.userIDs()}; for (const UserID &id : userIDs) { const Mailbox mbox = mailbox(id); if (!mbox.addrSpec().isEmpty()) { res.push_back(mbox); } } return res; } static std::vector extractMailboxes(const std::vector &signers) { std::vector res; for (const Key &i : signers) { const std::vector bxs = extractMailboxes(i); res.insert(res.end(), bxs.begin(), bxs.end()); } return res; } static bool keyContainsMailbox(const Key &key, const Mailbox &mbox) { const std::vector mbxs = extractMailboxes(key); return std::find_if(mbxs.cbegin(), mbxs.cend(), [mbox](const Mailbox &m) { return mailbox_equal(mbox, m, Qt::CaseInsensitive); }) != mbxs.cend(); } static bool keysContainMailbox(const std::vector &keys, const Mailbox &mbox) { return std::find_if(keys.cbegin(), keys.cend(), [mbox](const Key &key) { return keyContainsMailbox(key, mbox); }) != keys.cend(); } static bool relevantInDecryptVerifyContext(const VerificationResult &r) { // for D/V operations, we ignore verification results which are not errors and contain // no signatures (which means that the data was just not signed) return (r.error() && r.error().code() != GPG_ERR_DECRYPT_FAILED) || r.numSignatures() > 0; } static QString signatureSummaryToString(int summary) { if (summary & Signature::None) { return i18n("Error: Signature not verified"); } else if (summary & Signature::Valid || summary & Signature::Green) { return i18n("Good signature"); } else if (summary & Signature::KeyRevoked) { return i18n("Signing certificate was revoked"); } else if (summary & Signature::KeyExpired) { return i18n("Signing certificate is expired"); } else if (summary & Signature::KeyMissing) { return i18n("Certificate is not available"); } else if (summary & Signature::SigExpired) { return i18n("Signature expired"); } else if (summary & Signature::CrlMissing) { return i18n("CRL missing"); } else if (summary & Signature::CrlTooOld) { return i18n("CRL too old"); } else if (summary & Signature::BadPolicy) { return i18n("Bad policy"); } else if (summary & Signature::SysError) { return i18n("System error"); // ### retrieve system error details? } else if (summary & Signature::Red) { return i18n("Bad signature"); } return QString(); } static QString formatValidSignatureWithTrustLevel(const UserID &id) { if (id.isNull()) { return QString(); } switch (id.validity()) { case UserID::Marginal: return i18n("The signature is valid but the trust in the certificate's validity is only marginal."); case UserID::Full: return i18n("The signature is valid and the certificate's validity is fully trusted."); case UserID::Ultimate: return i18n("The signature is valid and the certificate's validity is ultimately trusted."); case UserID::Never: return i18n("The signature is valid but the certificate's validity is not trusted."); case UserID::Unknown: return i18n("The signature is valid but the certificate's validity is unknown."); case UserID::Undefined: default: return i18n("The signature is valid but the certificate's validity is undefined."); } } static QString renderKeyLink(const QString &fpr, const QString &text) { return QStringLiteral("%2").arg(fpr, text); } static QString renderKey(const Key &key) { if (key.isNull()) { return i18n("Unknown certificate"); } if (key.primaryFingerprint() && strlen(key.primaryFingerprint()) > 16 && key.numUserIDs()) { const QString text = QStringLiteral("%1 (%2)") .arg(Formatting::prettyNameAndEMail(key).toHtmlEscaped()) .arg(Formatting::prettyID(QString::fromLocal8Bit(key.primaryFingerprint()).right(16).toLatin1().constData())); return renderKeyLink(QLatin1String(key.primaryFingerprint()), text); } return renderKeyLink(QLatin1String(key.primaryFingerprint()), Formatting::prettyID(key.primaryFingerprint())); } static QString renderKeyEMailOnlyNameAsFallback(const Key &key) { if (key.isNull()) { return i18n("Unknown certificate"); } const QString email = Formatting::prettyEMail(key); const QString user = !email.isEmpty() ? email : Formatting::prettyName(key); return renderKeyLink(QLatin1String(key.primaryFingerprint()), user); } static QString formatDate(const QDateTime &dt) { return QLocale().toString(dt); } static QString formatSigningInformation(const Signature &sig) { if (sig.isNull()) { return QString(); } const QDateTime dt = sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime(); QString text; Key key = sig.key(); if (dt.isValid()) { text = i18nc("1 is a date", "Signature created on %1", formatDate(dt)) + QStringLiteral("
"); } if (key.isNull()) { return text += i18n("With unavailable certificate:") + QStringLiteral("
ID: 0x%1").arg(QString::fromLatin1(sig.fingerprint()).toUpper()); } text += i18n("With certificate:") + QStringLiteral("
") + renderKey(key); if (DeVSCompliance::isCompliant()) { text += (QStringLiteral("
") + (sig.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The signature is %1", DeVSCompliance::name(true)) : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The signature is not %1.", DeVSCompliance::name(true)))); } return text; } static QString strikeOut(const QString &str, bool strike) { return QString(strike ? QStringLiteral("%1") : QStringLiteral("%1")).arg(str.toHtmlEscaped()); } static QString formatInputOutputLabel(const QString &input, const QString &output, bool inputDeleted, bool outputDeleted) { if (output.isEmpty()) { return strikeOut(input, inputDeleted); } return i18nc("Input file --> Output file (rarr is arrow", "%1 → %2", strikeOut(input, inputDeleted), strikeOut(output, outputDeleted)); } static bool IsErrorOrCanceled(const GpgME::Error &err) { return err || err.isCanceled(); } static bool IsErrorOrCanceled(const Result &res) { return IsErrorOrCanceled(res.error()); } static bool IsBad(const Signature &sig) { return sig.summary() & Signature::Red; } static bool IsGoodOrValid(const Signature &sig) { return (sig.summary() & Signature::Valid) || (sig.summary() & Signature::Green); } static UserID findUserIDByMailbox(const Key &key, const Mailbox &mbox) { const auto userIDs{key.userIDs()}; for (const UserID &id : userIDs) if (mailbox_equal(mailbox(id), mbox, Qt::CaseInsensitive)) { return id; } return UserID(); } static void updateKeys(const VerificationResult &result) { // This little hack works around the problem that GnuPG / GpgME does not // provide Key information in a verification result. The Key object is // a dummy just holding the KeyID. This hack ensures that all available // keys are fetched from the backend and are populated for (const auto &sig : result.signatures()) { // Update key information sig.key(true, true); } } static QString ensureUniqueDirectory(const QString &path) { // make sure that we don't use an existing directory QString uniquePath = path; const QFileInfo outputInfo{path}; if (outputInfo.exists()) { const auto uniqueName = KFileUtils::suggestName(QUrl::fromLocalFile(outputInfo.absolutePath()), outputInfo.fileName()); uniquePath = outputInfo.dir().filePath(uniqueName); } if (!QDir{}.mkpath(uniquePath)) { return {}; } return uniquePath; } static bool mimeTypeInherits(const QMimeType &mimeType, const QString &mimeTypeName) { // inherits is expensive on an invalid mimeType return mimeType.isValid() && mimeType.inherits(mimeTypeName); } } class DecryptVerifyResult::SenderInfo { public: explicit SenderInfo(const Mailbox &infSender, const std::vector &signers_) : informativeSender(infSender) , signers(signers_) { } const Mailbox informativeSender; const std::vector signers; bool hasInformativeSender() const { return !informativeSender.addrSpec().isEmpty(); } bool conflicts() const { return hasInformativeSender() && hasKeys() && !keysContainMailbox(signers, informativeSender); } bool hasKeys() const { return std::any_of(signers.cbegin(), signers.cend(), [](const Key &key) { return !key.isNull(); }); } std::vector signerMailboxes() const { return extractMailboxes(signers); } }; namespace { static Task::Result::VisualCode codeForVerificationResult(const VerificationResult &res) { if (res.isNull()) { return Task::Result::NeutralSuccess; } const std::vector sigs = res.signatures(); if (sigs.empty()) { return Task::Result::Warning; } if (std::find_if(sigs.begin(), sigs.end(), IsBad) != sigs.end()) { return Task::Result::Danger; } if ((size_t)std::count_if(sigs.begin(), sigs.end(), IsGoodOrValid) == sigs.size()) { return Task::Result::AllGood; } return Task::Result::Warning; } static QString formatVerificationResultOverview(const VerificationResult &res, const DecryptVerifyResult::SenderInfo &info) { if (res.isNull()) { return QString(); } const Error err = res.error(); if (err.isCanceled()) { return i18n("Verification canceled."); } else if (err) { return i18n("Verification failed: %1.", Formatting::errorAsString(err).toHtmlEscaped()); } const std::vector sigs = res.signatures(); if (sigs.empty()) { return i18n("No signatures found."); } const uint bad = std::count_if(sigs.cbegin(), sigs.cend(), IsBad); if (bad > 0) { return i18np("Invalid signature.", "%1 invalid signatures.", bad); } const uint warn = std::count_if(sigs.cbegin(), sigs.cend(), [](const Signature &sig) { return !IsGoodOrValid(sig); }); if (warn == sigs.size()) { return i18np("The data could not be verified.", "%1 signatures could not be verified.", warn); } // Good signature: QString text; if (sigs.size() == 1) { text = i18n("Valid signature by %1", renderKeyEMailOnlyNameAsFallback(sigs[0].key())); if (info.conflicts()) text += i18n("
Warning: The sender's mail address is not stored in the %1 used for signing.", renderKeyLink(QLatin1String(sigs[0].key().primaryFingerprint()), i18n("certificate"))); } else { text = i18np("Valid signature.", "%1 valid signatures.", sigs.size()); if (info.conflicts()) { text += i18n("
Warning: The sender's mail address is not stored in the certificates used for signing."); } } return text; } static QString formatDecryptionResultOverview(const DecryptionResult &result, const QString &errorString = QString()) { const Error err = result.error(); if (err.isCanceled()) { return i18n("Decryption canceled."); } else if (result.isLegacyCipherNoMDC()) { return i18n("Decryption failed: %1.", i18n("No integrity protection (MDC).")); } else if (!errorString.isEmpty()) { return i18n("Decryption failed: %1.", errorString.toHtmlEscaped()); } else if (err) { return i18n("Decryption failed: %1.", Formatting::errorAsString(err).toHtmlEscaped()); } return i18n("Decryption succeeded."); } static QString formatSignature(const Signature &sig, const DecryptVerifyResult::SenderInfo &info) { if (sig.isNull()) { return QString(); } const QString text = formatSigningInformation(sig) + QLatin1String("
"); const Key key = sig.key(); // Green if (sig.summary() & Signature::Valid) { const UserID id = findUserIDByMailbox(key, info.informativeSender); return text + formatValidSignatureWithTrustLevel(!id.isNull() ? id : key.userID(0)); } // Red if ((sig.summary() & Signature::Red)) { const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); if (sig.summary() & Signature::SysError) { return ret + QStringLiteral(" (%1)").arg(Formatting::errorAsString(sig.status())); } return ret; } // Key missing if ((sig.summary() & Signature::KeyMissing)) { return text + i18n("You can search the certificate on a keyserver or import it from a file."); } // Yellow if ((sig.validity() & Signature::Validity::Undefined) // || (sig.validity() & Signature::Validity::Unknown) // || (sig.summary() == Signature::Summary::None)) { return text + (key.protocol() == OpenPGP ? i18n("The used key is not certified by you or any trusted person.") : i18n("The used certificate is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown.")); } // Catch all fall through const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); if (sig.summary() & Signature::SysError) { return ret + QStringLiteral(" (%1)").arg(Formatting::errorAsString(sig.status())); } return ret; } static QStringList format(const std::vector &mbxs) { QStringList res; std::transform(mbxs.cbegin(), mbxs.cend(), std::back_inserter(res), [](const Mailbox &mbox) { return mbox.prettyAddress(); }); return res; } static QString formatVerificationResultDetails(const VerificationResult &res, const DecryptVerifyResult::SenderInfo &info, const QString &errorString) { if ((res.error().code() == GPG_ERR_EIO || res.error().code() == GPG_ERR_NO_DATA) && !errorString.isEmpty()) { return i18n("Input error: %1", errorString); } const std::vector sigs = res.signatures(); QString details; for (const Signature &sig : sigs) { details += formatSignature(sig, info) + QLatin1Char('\n'); } details = details.trimmed(); details.replace(QLatin1Char('\n'), QStringLiteral("

")); if (info.conflicts()) { details += i18n("

The sender's address %1 is not stored in the certificate. Stored: %2

", info.informativeSender.prettyAddress(), format(info.signerMailboxes()).join(i18nc("separator for a list of e-mail addresses", ", "))); } return details; } static QString formatRecipientsDetails(const std::vector &knownRecipients, unsigned int numRecipients) { if (numRecipients == 0) { return {}; } if (knownRecipients.empty()) { return QLatin1String("") + i18np("One unknown recipient.", "%1 unknown recipients.", numRecipients) + QLatin1String(""); } QString details = i18np("Recipient:", "Recipients:", numRecipients); if (numRecipients == 1) { details += QLatin1Char(' ') + renderKey(knownRecipients.front()); } else { details += QLatin1String("
    "); for (const Key &key : knownRecipients) { details += QLatin1String("
  • ") + renderKey(key) + QLatin1String("
  • "); } if (knownRecipients.size() < numRecipients) { details += QLatin1String("
  • ") + i18np("One unknown recipient", "%1 unknown recipients", numRecipients - knownRecipients.size()) + QLatin1String("
  • "); } details += QLatin1String("
"); } return details; } static QString formatDecryptionResultDetails(const DecryptionResult &res, const std::vector &recipients, const QString &errorString, bool isSigned, const QPointer &task) { if ((res.error().code() == GPG_ERR_EIO || res.error().code() == GPG_ERR_NO_DATA) && !errorString.isEmpty()) { return i18n("Input error: %1", errorString); } if (res.isNull() || res.error() || res.error().isCanceled()) { return formatRecipientsDetails(recipients, res.numRecipients()); } QString details; if (DeVSCompliance::isCompliant()) { details += ((res.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The decryption is %1.", DeVSCompliance::name(true)) : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The decryption is not %1.", DeVSCompliance::name(true))) + QStringLiteral("
")); } if (res.fileName()) { const auto decVerifyTask = qobject_cast(task.data()); if (decVerifyTask) { const auto embedFileName = QString::fromUtf8(res.fileName()).toHtmlEscaped(); if (embedFileName != decVerifyTask->outputLabel()) { details += i18n("Embedded file name: '%1'", embedFileName); details += QStringLiteral("
"); } } } if (!isSigned) { details += i18n("Note: You cannot be sure who encrypted this message as it is not signed.") + QLatin1String("
"); } if (res.isLegacyCipherNoMDC()) { details += i18nc("Integrity protection was missing because an old cipher was used.", "Hint: If this file was encrypted before the year 2003 it is " "likely that the file is legitimate. This is because back " "then integrity protection was not widely used.") + QStringLiteral("

") + i18nc("The user is offered to force decrypt a non integrity protected message. With the strong advice to re-encrypt it.", "If you are confident that the file was not manipulated you should re-encrypt it after you have forced the decryption.") + QStringLiteral("

"); } details += formatRecipientsDetails(recipients, res.numRecipients()); return details; } static QString formatDecryptVerifyResultOverview(const DecryptionResult &dr, const VerificationResult &vr, const DecryptVerifyResult::SenderInfo &info) { if (IsErrorOrCanceled(dr) || !relevantInDecryptVerifyContext(vr)) { return formatDecryptionResultOverview(dr); } return formatVerificationResultOverview(vr, info); } static QString formatDecryptVerifyResultDetails(const DecryptionResult &dr, const VerificationResult &vr, const std::vector &recipients, const DecryptVerifyResult::SenderInfo &info, const QString &errorString, const QPointer &task) { const QString drDetails = formatDecryptionResultDetails(dr, recipients, errorString, relevantInDecryptVerifyContext(vr), task); if (IsErrorOrCanceled(dr) || !relevantInDecryptVerifyContext(vr)) { return drDetails; } return drDetails + (drDetails.isEmpty() ? QString() : QStringLiteral("
")) + formatVerificationResultDetails(vr, info, errorString); } } // anon namespace class DecryptVerifyResult::Private { DecryptVerifyResult *const q; public: Private(DecryptVerifyOperation type, const VerificationResult &vr, const DecryptionResult &dr, const QByteArray &stuff, const QString &fileName, const GpgME::Error &error, const QString &errString, const QString &input, const QString &output, const AuditLogEntry &auditLog, Task *parentTask, const Mailbox &informativeSender, DecryptVerifyResult *qq) : q(qq) , m_type(type) , m_verificationResult(vr) , m_decryptionResult(dr) , m_stuff(stuff) , m_fileName(fileName) , m_error(error) , m_errorString(errString) , m_inputLabel(input) , m_outputLabel(output) , m_auditLog(auditLog) , m_parentTask(QPointer(parentTask)) , m_informativeSender(informativeSender) { } QString label() const { return formatInputOutputLabel(m_inputLabel, m_outputLabel, false, q->hasError()); } DecryptVerifyResult::SenderInfo makeSenderInfo() const; bool isDecryptOnly() const { return m_type == Decrypt; } bool isVerifyOnly() const { return m_type == Verify; } bool isDecryptVerify() const { return m_type == DecryptVerify; } DecryptVerifyOperation m_type; VerificationResult m_verificationResult; DecryptionResult m_decryptionResult; QByteArray m_stuff; QString m_fileName; GpgME::Error m_error; QString m_errorString; QString m_inputLabel; QString m_outputLabel; const AuditLogEntry m_auditLog; QPointer m_parentTask; const Mailbox m_informativeSender; }; DecryptVerifyResult::SenderInfo DecryptVerifyResult::Private::makeSenderInfo() const { return SenderInfo(m_informativeSender, KeyCache::instance()->findSigners(m_verificationResult)); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptResult(const DecryptionResult &dr, const QByteArray &plaintext, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Decrypt, // VerificationResult(), dr, plaintext, {}, {}, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptResult(const GpgME::Error &err, const QString &what, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Decrypt, // VerificationResult(), DecryptionResult(err), QByteArray(), {}, err, what, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptVerifyResult(const DecryptionResult &dr, const VerificationResult &vr, const QByteArray &plaintext, const QString &fileName, const AuditLogEntry &auditLog) { const auto err = dr.error().code() ? dr.error() : vr.error(); return std::shared_ptr(new DecryptVerifyResult(DecryptVerify, // vr, dr, plaintext, fileName, err, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromDecryptVerifyResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(DecryptVerify, // VerificationResult(), DecryptionResult(err), QByteArray(), {}, err, details, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyOpaqueResult(const VerificationResult &vr, const QByteArray &plaintext, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // vr, DecryptionResult(), plaintext, {}, {}, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyOpaqueResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // VerificationResult(err), DecryptionResult(), QByteArray(), {}, err, details, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyDetachedResult(const VerificationResult &vr, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // vr, DecryptionResult(), QByteArray(), {}, {}, QString(), inputLabel(), outputLabel(), auditLog, this, informativeSender())); } std::shared_ptr AbstractDecryptVerifyTask::fromVerifyDetachedResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog) { return std::shared_ptr(new DecryptVerifyResult(Verify, // VerificationResult(err), DecryptionResult(), QByteArray(), {}, err, details, inputLabel(), outputLabel(), auditLog, this, informativeSender())); } DecryptVerifyResult::DecryptVerifyResult(DecryptVerifyOperation type, const VerificationResult &vr, const DecryptionResult &dr, const QByteArray &stuff, const QString &fileName, const GpgME::Error &error, const QString &errString, const QString &inputLabel, const QString &outputLabel, const AuditLogEntry &auditLog, Task *parentTask, const Mailbox &informativeSender) : Task::Result() , d(new Private(type, vr, dr, stuff, fileName, error, errString, inputLabel, outputLabel, auditLog, parentTask, informativeSender, this)) { } Task::Result::ContentType DecryptVerifyResult::viewableContentType() const { #if QGPGME_SUPPORTS_IS_MIME if (decryptionResult().isMime()) { return Task::Result::ContentType::Mime; } #endif if (fileName().endsWith(QStringLiteral("openpgp-encrypted-message"))) { return Task::Result::ContentType::Mime; } QMimeDatabase mimeDatabase; const auto mimeType = mimeDatabase.mimeTypeForFile(fileName()); if (mimeTypeInherits(mimeType, QStringLiteral("message/rfc822"))) { return Task::Result::ContentType::Mime; } if (mimeTypeInherits(mimeType, QStringLiteral("application/mbox"))) { return Task::Result::ContentType::Mbox; } return Task::Result::ContentType::None; } QString DecryptVerifyResult::overview() const { QString ov; if (d->isDecryptOnly()) { ov += formatDecryptionResultOverview(d->m_decryptionResult); } else if (d->isVerifyOnly()) { ov += formatVerificationResultOverview(d->m_verificationResult, d->makeSenderInfo()); } else { ov += formatDecryptVerifyResultOverview(d->m_decryptionResult, d->m_verificationResult, d->makeSenderInfo()); } if (ov.size() + d->label().size() > 120) { // Avoid ugly breaks ov = QStringLiteral("
") + ov; } return i18nc("label: result example: foo.sig: Verification failed. ", "%1: %2", d->label(), ov); } QString DecryptVerifyResult::details() const { if (d->isDecryptOnly()) { return formatDecryptionResultDetails(d->m_decryptionResult, KeyCache::instance()->findRecipients(d->m_decryptionResult), errorString(), false, d->m_parentTask); } if (d->isVerifyOnly()) { return formatVerificationResultDetails(d->m_verificationResult, d->makeSenderInfo(), errorString()); } return formatDecryptVerifyResultDetails(d->m_decryptionResult, d->m_verificationResult, KeyCache::instance()->findRecipients(d->m_decryptionResult), d->makeSenderInfo(), errorString(), d->m_parentTask); } GpgME::Error DecryptVerifyResult::error() const { return d->m_error; } QString DecryptVerifyResult::errorString() const { return d->m_errorString; } AuditLogEntry DecryptVerifyResult::auditLog() const { return d->m_auditLog; } QPointer DecryptVerifyResult::parentTask() const { return d->m_parentTask; } Task::Result::VisualCode DecryptVerifyResult::code() const { if ((d->m_type == DecryptVerify || d->m_type == Verify) && relevantInDecryptVerifyContext(verificationResult())) { return codeForVerificationResult(verificationResult()); } return hasError() ? NeutralError : NeutralSuccess; } GpgME::VerificationResult DecryptVerifyResult::verificationResult() const { return d->m_verificationResult; } GpgME::DecryptionResult DecryptVerifyResult::decryptionResult() const { return d->m_decryptionResult; } QString DecryptVerifyResult::fileName() const { return d->m_fileName; } class AbstractDecryptVerifyTask::Private { public: Mailbox informativeSender; QPointer job; }; AbstractDecryptVerifyTask::AbstractDecryptVerifyTask(QObject *parent) : Task(parent) , d(new Private) { } AbstractDecryptVerifyTask::~AbstractDecryptVerifyTask() { } void AbstractDecryptVerifyTask::cancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->job) { d->job->slotCancel(); } } Mailbox AbstractDecryptVerifyTask::informativeSender() const { return d->informativeSender; } void AbstractDecryptVerifyTask::setInformativeSender(const Mailbox &sender) { d->informativeSender = sender; } QGpgME::Job *AbstractDecryptVerifyTask::job() const { return d->job; } void AbstractDecryptVerifyTask::setJob(QGpgME::Job *job) { d->job = job; } class DecryptVerifyTask::Private { DecryptVerifyTask *const q; public: explicit Private(DecryptVerifyTask *qq) : q{qq} { } void startDecryptVerifyJob(); void startDecryptVerifyArchiveJob(); void slotResult(const DecryptionResult &, const VerificationResult &, const QByteArray & = {}); std::shared_ptr m_input; std::shared_ptr m_output; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; bool m_ignoreMDCError = false; bool m_extractArchive = false; QString m_inputFilePath; + QString m_outputFilePath; QString m_outputDirectory; }; void DecryptVerifyTask::Private::slotResult(const DecryptionResult &dr, const VerificationResult &vr, const QByteArray &plainText) { updateKeys(vr); { std::stringstream ss; ss << dr << '\n' << vr; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); if (m_output) { if (dr.error().code() || vr.error().code()) { m_output->cancel(); } else { try { kleo_assert(!dr.isNull() || !vr.isNull()); m_output->finalize(); } catch (const GpgME::Exception &e) { q->emitResult(q->fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } catch (const std::exception &e) { q->emitResult( q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); return; } catch (...) { q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); return; } } } const int drErr = dr.error().code(); const QString errorString = m_output ? m_output->errorString() : QString{}; if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || (m_output && m_output->failed())) { q->emitResult(q->fromDecryptResult(drErr ? dr.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog)); return; } q->emitResult(q->fromDecryptVerifyResult(dr, vr, plainText, m_output ? m_output->fileName() : QString{}, auditLog)); } DecryptVerifyTask::DecryptVerifyTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } DecryptVerifyTask::~DecryptVerifyTask() { } void DecryptVerifyTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void DecryptVerifyTask::setOutput(const std::shared_ptr &output) { d->m_output = output; kleo_assert(d->m_output && d->m_output->ioDevice()); } void DecryptVerifyTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = prot == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void DecryptVerifyTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception( gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this is an S/MIME or an OpenPGP signature/ciphertext - maybe it is neither ciphertext nor a signature?"), Exception::MessageOnly); } setProtocol(p); } QString DecryptVerifyTask::label() const { - return i18n("Decrypting: %1...", d->m_input->label()); + return i18n("Decrypting %1...", inputLabel()); } unsigned long long DecryptVerifyTask::inputSize() const { return d->m_input ? d->m_input->size() : 0; } QString DecryptVerifyTask::inputLabel() const { - return d->m_input ? d->m_input->label() : QString(); + return d->m_input ? d->m_input->label() : QFileInfo{d->m_inputFilePath}.fileName(); } QString DecryptVerifyTask::outputLabel() const { - return d->m_output ? d->m_output->label() : d->m_outputDirectory; + if (d->m_output) { + return d->m_output->label(); + } else if (!d->m_outputFilePath.isEmpty()) { + return QFileInfo{d->m_outputFilePath}.fileName(); + } else { + return d->m_outputDirectory; + } } Protocol DecryptVerifyTask::protocol() const { return d->m_protocol; } static void ensureIOOpen(QIODevice *input, QIODevice *output) { if (input && !input->isOpen()) { input->open(QIODevice::ReadOnly); } if (output && !output->isOpen()) { output->open(QIODevice::WriteOnly); } } void DecryptVerifyTask::setIgnoreMDCError(bool value) { d->m_ignoreMDCError = value; } void DecryptVerifyTask::setExtractArchive(bool extract) { d->m_extractArchive = extract; } void DecryptVerifyTask::setInputFile(const QString &path) { d->m_inputFilePath = path; } +void DecryptVerifyTask::setOutputFile(const QString &path) +{ + d->m_outputFilePath = path; +} + void DecryptVerifyTask::setOutputDirectory(const QString &directory) { d->m_outputDirectory = directory; } static bool archiveJobsCanBeUsed(GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::DecryptVerifyArchiveJob::isSupported(); } void DecryptVerifyTask::doStart() { kleo_assert(d->m_backend); if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) { d->startDecryptVerifyArchiveJob(); } else { d->startDecryptVerifyJob(); } } static void setIgnoreMDCErrorFlag(QGpgME::Job *job, bool ignoreMDCError) { if (ignoreMDCError) { qCDebug(KLEOPATRA_LOG) << "Modifying job to ignore MDC errors."; auto ctx = QGpgME::Job::context(job); if (!ctx) { qCWarning(KLEOPATRA_LOG) << "Failed to get context for job"; } else { const auto err = ctx->setFlag("ignore-mdc-error", "1"); if (err) { qCWarning(KLEOPATRA_LOG) << "Failed to set ignore mdc errors" << Formatting::errorAsString(err); } } } } void DecryptVerifyTask::Private::startDecryptVerifyJob() { +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (QFile::exists(m_outputFilePath)) { + // The output files are always written to a temporary location. Therefore, this can only occur + // if two signed/encrypted files with the same name in different folders are verified/decrypted + // because they would be written to the same temporary location. + QMetaObject::invokeMethod( + q, + [this]() { + slotResult(DecryptionResult{Error::fromCode(GPG_ERR_EEXIST)}, VerificationResult{}); + }, + Qt::QueuedConnection); + return; + } +#endif try { std::unique_ptr job{m_backend->decryptVerifyJob()}; kleo_assert(job); setIgnoreMDCErrorFlag(job.get(), m_ignoreMDCError); QObject::connect(job.get(), &QGpgME::DecryptVerifyJob::result, q, [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult, const QByteArray &plainText) { slotResult(decryptResult, verifyResult, plainText); }); connect(job.get(), &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + job->setInputFile(m_inputFilePath); + job->setOutputFile(m_outputFilePath); + const auto err = job->startIt(); +#else ensureIOOpen(m_input->ioDevice().get(), m_output->ioDevice().get()); job->start(m_input->ioDevice(), m_output->ioDevice()); +#endif q->setJob(job.release()); } catch (const GpgME::Exception &e) { q->emitResult(q->fromDecryptVerifyResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { q->emitResult( q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } void DecryptVerifyTask::Private::startDecryptVerifyArchiveJob() { std::unique_ptr job{m_backend->decryptVerifyArchiveJob()}; kleo_assert(job); setIgnoreMDCErrorFlag(job.get(), m_ignoreMDCError); connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::result, q, [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult) { slotResult(decryptResult, verifyResult); }); connect(job.get(), &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress); #if QGPGME_ARCHIVE_JOBS_SUPPORT_INPUT_FILENAME // make sure that we don't use an existing output directory const auto outputDirectory = ensureUniqueDirectory(m_outputDirectory); if (outputDirectory.isEmpty()) { q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_GENERAL), {}, {})); return; } m_outputDirectory = outputDirectory; job->setInputFile(m_inputFilePath); job->setOutputDirectory(m_outputDirectory); const auto err = job->startIt(); #else ensureIOOpen(m_input->ioDevice().get(), nullptr); job->setOutputDirectory(m_outputDirectory); const auto err = job->start(m_input->ioDevice()); #endif q->setJob(job.release()); if (err) { q->emitResult(q->fromDecryptVerifyResult(err, {}, {})); } } class DecryptTask::Private { DecryptTask *const q; public: explicit Private(DecryptTask *qq) : q{qq} { } void slotResult(const DecryptionResult &, const QByteArray &); void registerJob(QGpgME::DecryptJob *job) { q->connect(job, SIGNAL(result(GpgME::DecryptionResult, QByteArray)), q, SLOT(slotResult(GpgME::DecryptionResult, QByteArray))); q->connect(job, &QGpgME::Job::jobProgress, q, &DecryptTask::setProgress); } std::shared_ptr m_input; std::shared_ptr m_output; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; }; void DecryptTask::Private::slotResult(const DecryptionResult &result, const QByteArray &plainText) { { std::stringstream ss; ss << result; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); if (result.error().code()) { m_output->cancel(); } else { try { kleo_assert(!result.isNull()); m_output->finalize(); } catch (const GpgME::Exception &e) { q->emitResult(q->fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } catch (const std::exception &e) { q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); return; } catch (...) { q->emitResult(q->fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); return; } } const int drErr = result.error().code(); const QString errorString = m_output->errorString(); if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || m_output->failed()) { q->emitResult(q->fromDecryptResult(result.error() ? result.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog)); return; } q->emitResult(q->fromDecryptResult(result, plainText, auditLog)); } DecryptTask::DecryptTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } DecryptTask::~DecryptTask() { } void DecryptTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void DecryptTask::setOutput(const std::shared_ptr &output) { d->m_output = output; kleo_assert(d->m_output && d->m_output->ioDevice()); } void DecryptTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void DecryptTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this was S/MIME- or OpenPGP-encrypted - maybe it is not ciphertext at all?"), Exception::MessageOnly); } setProtocol(p); } QString DecryptTask::label() const { return i18n("Decrypting: %1...", d->m_input->label()); } unsigned long long DecryptTask::inputSize() const { return d->m_input ? d->m_input->size() : 0; } QString DecryptTask::inputLabel() const { return d->m_input ? d->m_input->label() : QString(); } QString DecryptTask::outputLabel() const { return d->m_output ? d->m_output->label() : QString(); } Protocol DecryptTask::protocol() const { return d->m_protocol; } void DecryptTask::doStart() { kleo_assert(d->m_backend); try { std::unique_ptr job{d->m_backend->decryptJob()}; kleo_assert(job); d->registerJob(job.get()); ensureIOOpen(d->m_input->ioDevice().get(), d->m_output->ioDevice().get()); job->start(d->m_input->ioDevice(), d->m_output->ioDevice()); setJob(job.release()); } catch (const GpgME::Exception &e) { emitResult(fromDecryptResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { emitResult(fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { emitResult(fromDecryptResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } class VerifyOpaqueTask::Private { VerifyOpaqueTask *const q; public: explicit Private(VerifyOpaqueTask *qq) : q{qq} { } void startVerifyOpaqueJob(); void startDecryptVerifyArchiveJob(); void slotResult(const VerificationResult &, const QByteArray & = {}); std::shared_ptr m_input; std::shared_ptr m_output; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; bool m_extractArchive = false; QString m_inputFilePath; + QString m_outputFilePath; QString m_outputDirectory; }; void VerifyOpaqueTask::Private::slotResult(const VerificationResult &result, const QByteArray &plainText) { updateKeys(result); { std::stringstream ss; ss << result; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); if (m_output) { if (result.error().code()) { m_output->cancel(); } else { try { kleo_assert(!result.isNull()); m_output->finalize(); } catch (const GpgME::Exception &e) { q->emitResult(q->fromVerifyOpaqueResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } catch (const std::exception &e) { q->emitResult( q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); return; } catch (...) { q->emitResult(q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); return; } } } const int drErr = result.error().code(); const QString errorString = m_output ? m_output->errorString() : QString{}; if (((drErr == GPG_ERR_EIO || drErr == GPG_ERR_NO_DATA) && !errorString.isEmpty()) || (m_output && m_output->failed())) { q->emitResult(q->fromVerifyOpaqueResult(result.error() ? result.error() : Error::fromCode(GPG_ERR_EIO), errorString, auditLog)); return; } q->emitResult(q->fromVerifyOpaqueResult(result, plainText, auditLog)); } VerifyOpaqueTask::VerifyOpaqueTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } VerifyOpaqueTask::~VerifyOpaqueTask() { } void VerifyOpaqueTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void VerifyOpaqueTask::setOutput(const std::shared_ptr &output) { d->m_output = output; kleo_assert(d->m_output && d->m_output->ioDevice()); } void VerifyOpaqueTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void VerifyOpaqueTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this is an S/MIME or an OpenPGP signature - maybe it is not a signature at all?"), Exception::MessageOnly); } setProtocol(p); } QString VerifyOpaqueTask::label() const { - return i18n("Verifying: %1...", d->m_input->label()); + return i18n("Verifying %1...", inputLabel()); } unsigned long long VerifyOpaqueTask::inputSize() const { return d->m_input ? d->m_input->size() : 0; } QString VerifyOpaqueTask::inputLabel() const { - return d->m_input ? d->m_input->label() : QString(); + return d->m_input ? d->m_input->label() : QFileInfo{d->m_inputFilePath}.fileName(); } QString VerifyOpaqueTask::outputLabel() const { - return d->m_output ? d->m_output->label() : d->m_outputDirectory; + if (d->m_output) { + return d->m_output->label(); + } else if (!d->m_outputFilePath.isEmpty()) { + return QFileInfo{d->m_outputFilePath}.fileName(); + } else { + return d->m_outputDirectory; + } } Protocol VerifyOpaqueTask::protocol() const { return d->m_protocol; } void VerifyOpaqueTask::setExtractArchive(bool extract) { d->m_extractArchive = extract; } void VerifyOpaqueTask::setInputFile(const QString &path) { d->m_inputFilePath = path; } +void VerifyOpaqueTask::setOutputFile(const QString &path) +{ + d->m_outputFilePath = path; +} + void VerifyOpaqueTask::setOutputDirectory(const QString &directory) { d->m_outputDirectory = directory; } void VerifyOpaqueTask::doStart() { kleo_assert(d->m_backend); if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) { d->startDecryptVerifyArchiveJob(); } else { d->startVerifyOpaqueJob(); } } void VerifyOpaqueTask::Private::startVerifyOpaqueJob() { +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (QFile::exists(m_outputFilePath)) { + // The output files are always written to a temporary location. Therefore, this can only occur + // if two signed/encrypted files with the same name in different folders are verified/decrypted + // because they would be written to the same temporary location. + QMetaObject::invokeMethod( + q, + [this]() { + slotResult(VerificationResult{Error::fromCode(GPG_ERR_EEXIST)}); + }, + Qt::QueuedConnection); + return; + } +#endif try { std::unique_ptr job{m_backend->verifyOpaqueJob()}; kleo_assert(job); connect(job.get(), &QGpgME::VerifyOpaqueJob::result, q, [this](const GpgME::VerificationResult &result, const QByteArray &plainText) { slotResult(result, plainText); }); connect(job.get(), &QGpgME::Job::jobProgress, q, &VerifyOpaqueTask::setProgress); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + job->setInputFile(m_inputFilePath); + job->setOutputFile(m_outputFilePath); + const auto err = job->startIt(); +#else ensureIOOpen(m_input->ioDevice().get(), m_output ? m_output->ioDevice().get() : nullptr); job->start(m_input->ioDevice(), m_output ? m_output->ioDevice() : std::shared_ptr()); +#endif q->setJob(job.release()); } catch (const GpgME::Exception &e) { q->emitResult(q->fromVerifyOpaqueResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { q->emitResult( q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { q->emitResult(q->fromVerifyOpaqueResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } void VerifyOpaqueTask::Private::startDecryptVerifyArchiveJob() { std::unique_ptr job{m_backend->decryptVerifyArchiveJob()}; kleo_assert(job); connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::result, q, [this](const DecryptionResult &, const VerificationResult &verifyResult) { slotResult(verifyResult); }); connect(job.get(), &QGpgME::DecryptVerifyArchiveJob::dataProgress, q, &VerifyOpaqueTask::setProgress); #if QGPGME_ARCHIVE_JOBS_SUPPORT_INPUT_FILENAME // make sure that we don't use an existing output directory const auto outputDirectory = ensureUniqueDirectory(m_outputDirectory); if (outputDirectory.isEmpty()) { q->emitResult(q->fromDecryptVerifyResult(Error::fromCode(GPG_ERR_GENERAL), {}, {})); return; } m_outputDirectory = outputDirectory; job->setInputFile(m_inputFilePath); job->setOutputDirectory(m_outputDirectory); const auto err = job->startIt(); #else ensureIOOpen(m_input->ioDevice().get(), nullptr); job->setOutputDirectory(m_outputDirectory); const auto err = job->start(m_input->ioDevice()); #endif q->setJob(job.release()); if (err) { q->emitResult(q->fromVerifyOpaqueResult(err, {}, {})); } } class VerifyDetachedTask::Private { VerifyDetachedTask *const q; public: explicit Private(VerifyDetachedTask *qq) : q{qq} { } void slotResult(const VerificationResult &); void registerJob(QGpgME::VerifyDetachedJob *job) { q->connect(job, SIGNAL(result(GpgME::VerificationResult)), q, SLOT(slotResult(GpgME::VerificationResult))); q->connect(job, &QGpgME::Job::jobProgress, q, &VerifyDetachedTask::setProgress); } + QString signatureLabel() const; + QString signedDataLabel() const; + std::shared_ptr m_input, m_signedData; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; + QString m_signatureFilePath; + QString m_signedFilePath; }; void VerifyDetachedTask::Private::slotResult(const VerificationResult &result) { updateKeys(result); { std::stringstream ss; ss << result; qCDebug(KLEOPATRA_LOG) << ss.str().c_str(); } const AuditLogEntry auditLog = auditLogFromSender(q->sender()); try { kleo_assert(!result.isNull()); q->emitResult(q->fromVerifyDetachedResult(result, auditLog)); } catch (const GpgME::Exception &e) { q->emitResult(q->fromVerifyDetachedResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); } catch (const std::exception &e) { q->emitResult(q->fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), auditLog)); } catch (...) { q->emitResult(q->fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), auditLog)); } } +QString VerifyDetachedTask::Private::signatureLabel() const +{ + return m_input ? m_input->label() : m_signatureFilePath; +} + +QString VerifyDetachedTask::Private::signedDataLabel() const +{ + return m_signedData ? m_signedData->label() : m_signedFilePath; +} + VerifyDetachedTask::VerifyDetachedTask(QObject *parent) : AbstractDecryptVerifyTask(parent) , d(new Private(this)) { } VerifyDetachedTask::~VerifyDetachedTask() { } void VerifyDetachedTask::setInput(const std::shared_ptr &input) { d->m_input = input; kleo_assert(d->m_input && d->m_input->ioDevice()); } void VerifyDetachedTask::setSignedData(const std::shared_ptr &signedData) { d->m_signedData = signedData; kleo_assert(d->m_signedData && d->m_signedData->ioDevice()); } +void VerifyDetachedTask::setSignatureFile(const QString &path) +{ + d->m_signatureFilePath = path; +} + +void VerifyDetachedTask::setSignedFile(const QString &path) +{ + d->m_signedFilePath = path; +} + void VerifyDetachedTask::setProtocol(Protocol prot) { kleo_assert(prot != UnknownProtocol); d->m_protocol = prot; d->m_backend = (prot == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(d->m_backend); } void VerifyDetachedTask::autodetectProtocolFromInput() { if (!d->m_input) { return; } const Protocol p = findProtocol(d->m_input->classification()); if (p == UnknownProtocol) { throw Exception(gpg_error(GPG_ERR_NOTHING_FOUND), i18n("Could not determine whether this is an S/MIME or an OpenPGP signature - maybe it is not a signature at all?"), Exception::MessageOnly); } setProtocol(p); } unsigned long long VerifyDetachedTask::inputSize() const { return d->m_signedData ? d->m_signedData->size() : 0; } QString VerifyDetachedTask::label() const { - if (d->m_signedData) { + const QString signedDataLabel = d->signedDataLabel(); + if (!signedDataLabel.isEmpty()) { return xi18nc( "Verification of a detached signature in progress. The first file contains the data." "The second file is the signature file.", - "Verifying: %1 with %2...", - d->m_signedData->label(), - d->m_input->label()); + "Verifying %1 with %2...", + signedDataLabel, + d->signatureLabel()); } - return i18n("Verifying signature: %1...", d->m_input->label()); + return i18n("Verifying signature %1...", d->signatureLabel()); } QString VerifyDetachedTask::inputLabel() const { - if (d->m_signedData && d->m_input) { + const QString signatureLabel = d->signatureLabel(); + const QString signedDataLabel = d->signedDataLabel(); + if (!signedDataLabel.isEmpty() && !signatureLabel.isEmpty()) { return xi18nc( "Verification of a detached signature summary. The first file contains the data." "The second file is signature.", "Verified %1 with %2", - d->m_signedData->label(), - d->m_input->label()); + signedDataLabel, + signatureLabel); } - return d->m_input ? d->m_input->label() : QString(); + return signatureLabel; } QString VerifyDetachedTask::outputLabel() const { return QString(); } Protocol VerifyDetachedTask::protocol() const { return d->m_protocol; } void VerifyDetachedTask::doStart() { kleo_assert(d->m_backend); try { std::unique_ptr job{d->m_backend->verifyDetachedJob()}; kleo_assert(job); d->registerJob(job.get()); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (d->m_protocol == GpgME::OpenPGP) { + job->setSignatureFile(d->m_signatureFilePath); + job->setSignedFile(d->m_signedFilePath); + job->startIt(); + } else { + ensureIOOpen(d->m_input->ioDevice().get(), nullptr); + ensureIOOpen(d->m_signedData->ioDevice().get(), nullptr); + job->start(d->m_input->ioDevice(), d->m_signedData->ioDevice()); + } +#else ensureIOOpen(d->m_input->ioDevice().get(), nullptr); ensureIOOpen(d->m_signedData->ioDevice().get(), nullptr); job->start(d->m_input->ioDevice(), d->m_signedData->ioDevice()); +#endif setJob(job.release()); } catch (const GpgME::Exception &e) { emitResult(fromVerifyDetachedResult(e.error(), QString::fromLocal8Bit(e.what()), AuditLogEntry())); } catch (const std::exception &e) { emitResult( fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught exception: %1", QString::fromLocal8Bit(e.what())), AuditLogEntry())); } catch (...) { emitResult(fromVerifyDetachedResult(Error::fromCode(GPG_ERR_INTERNAL), i18n("Caught unknown exception"), AuditLogEntry())); } } #include "moc_decryptverifytask.cpp" diff --git a/src/crypto/decryptverifytask.h b/src/crypto/decryptverifytask.h index 1ea4a0fba..eeecc7baa 100644 --- a/src/crypto/decryptverifytask.h +++ b/src/crypto/decryptverifytask.h @@ -1,267 +1,276 @@ /* -*- mode: c++; c-basic-offset:4 -*- decryptverifytask.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "task.h" #include #include #include namespace KMime { namespace Types { class Mailbox; } } namespace GpgME { class DecryptionResult; class VerificationResult; class Key; class Signature; } namespace QGpgME { class Job; } namespace Kleo { class Input; class Output; class AuditLogEntry; } namespace Kleo { namespace Crypto { class DecryptVerifyResult; class AbstractDecryptVerifyTask : public Task { Q_OBJECT public: explicit AbstractDecryptVerifyTask(QObject *parent = nullptr); ~AbstractDecryptVerifyTask() override; virtual void autodetectProtocolFromInput() = 0; KMime::Types::Mailbox informativeSender() const; void setInformativeSender(const KMime::Types::Mailbox &senders); virtual QString inputLabel() const = 0; virtual QString outputLabel() const = 0; public Q_SLOTS: void cancel() override; protected: std::shared_ptr fromDecryptResult(const GpgME::DecryptionResult &dr, const QByteArray &plaintext, const AuditLogEntry &auditLog); std::shared_ptr fromDecryptResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog); std::shared_ptr fromDecryptVerifyResult(const GpgME::DecryptionResult &dr, const GpgME::VerificationResult &vr, const QByteArray &plaintext, const QString &fileName, const AuditLogEntry &auditLog); std::shared_ptr fromDecryptVerifyResult(const GpgME::Error &err, const QString &what, const AuditLogEntry &auditLog); std::shared_ptr fromVerifyOpaqueResult(const GpgME::VerificationResult &vr, const QByteArray &plaintext, const AuditLogEntry &auditLog); std::shared_ptr fromVerifyOpaqueResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog); std::shared_ptr fromVerifyDetachedResult(const GpgME::VerificationResult &vr, const AuditLogEntry &auditLog); std::shared_ptr fromVerifyDetachedResult(const GpgME::Error &err, const QString &details, const AuditLogEntry &auditLog); public: // public to allow access from the Private classes of the concrete tasks QGpgME::Job *job() const; void setJob(QGpgME::Job *job); private: class Private; kdtools::pimpl_ptr d; }; class DecryptTask : public AbstractDecryptVerifyTask { Q_OBJECT public: explicit DecryptTask(QObject *parent = nullptr); ~DecryptTask() override; void setInput(const std::shared_ptr &input); void setOutput(const std::shared_ptr &output); void setProtocol(GpgME::Protocol prot); void autodetectProtocolFromInput() override; QString label() const override; GpgME::Protocol protocol() const override; QString inputLabel() const override; QString outputLabel() const override; private: void doStart() override; unsigned long long inputSize() const override; private: class Private; kdtools::pimpl_ptr d; Q_PRIVATE_SLOT(d, void slotResult(GpgME::DecryptionResult, QByteArray)) }; class VerifyDetachedTask : public AbstractDecryptVerifyTask { Q_OBJECT public: explicit VerifyDetachedTask(QObject *parent = nullptr); ~VerifyDetachedTask() override; void setInput(const std::shared_ptr &input); void setSignedData(const std::shared_ptr &signedData); + void setSignatureFile(const QString &path); + void setSignedFile(const QString &path); + void setProtocol(GpgME::Protocol prot); void autodetectProtocolFromInput() override; QString label() const override; GpgME::Protocol protocol() const override; QString inputLabel() const override; QString outputLabel() const override; private: void doStart() override; unsigned long long inputSize() const override; private: class Private; kdtools::pimpl_ptr d; Q_PRIVATE_SLOT(d, void slotResult(GpgME::VerificationResult)) }; class VerifyOpaqueTask : public AbstractDecryptVerifyTask { Q_OBJECT public: explicit VerifyOpaqueTask(QObject *parent = nullptr); ~VerifyOpaqueTask() override; void setInput(const std::shared_ptr &input); void setOutput(const std::shared_ptr &output); void setProtocol(GpgME::Protocol prot); void autodetectProtocolFromInput() override; QString label() const override; GpgME::Protocol protocol() const override; void setExtractArchive(bool extract); void setInputFile(const QString &path); + // for files + void setOutputFile(const QString &path); + // for archives void setOutputDirectory(const QString &directory); QString inputLabel() const override; QString outputLabel() const override; private: void doStart() override; unsigned long long inputSize() const override; private: class Private; kdtools::pimpl_ptr d; Q_PRIVATE_SLOT(d, void slotResult(GpgME::VerificationResult, QByteArray)) }; class DecryptVerifyTask : public AbstractDecryptVerifyTask { Q_OBJECT public: explicit DecryptVerifyTask(QObject *parent = nullptr); ~DecryptVerifyTask() override; void setInput(const std::shared_ptr &input); void setOutput(const std::shared_ptr &output); void setProtocol(GpgME::Protocol prot); void autodetectProtocolFromInput() override; QString label() const override; GpgME::Protocol protocol() const override; void setIgnoreMDCError(bool value); void setExtractArchive(bool extract); void setInputFile(const QString &path); + // for files + void setOutputFile(const QString &path); + // for archives void setOutputDirectory(const QString &directory); QString inputLabel() const override; QString outputLabel() const override; private: void doStart() override; unsigned long long inputSize() const override; private: class Private; kdtools::pimpl_ptr d; Q_PRIVATE_SLOT(d, void slotResult(GpgME::DecryptionResult, GpgME::VerificationResult, QByteArray)) }; class DecryptVerifyResult : public Task::Result { friend class ::Kleo::Crypto::AbstractDecryptVerifyTask; public: class SenderInfo; QString overview() const override; QString details() const override; GpgME::Error error() const override; QString errorString() const override; VisualCode code() const override; AuditLogEntry auditLog() const override; QPointer parentTask() const override; Task::Result::ContentType viewableContentType() const override; GpgME::VerificationResult verificationResult() const; GpgME::DecryptionResult decryptionResult() const; QString fileName() const; private: DecryptVerifyResult(); DecryptVerifyResult(const DecryptVerifyResult &); DecryptVerifyResult &operator=(const DecryptVerifyResult &other); DecryptVerifyResult(DecryptVerifyOperation op, const GpgME::VerificationResult &vr, const GpgME::DecryptionResult &dr, const QByteArray &stuff, const QString &fileName, const GpgME::Error &error, const QString &errString, const QString &inputLabel, const QString &outputLabel, const AuditLogEntry &auditLog, Task *parentTask, const KMime::Types::Mailbox &informativeSender); private: class Private; kdtools::pimpl_ptr d; }; } } diff --git a/src/crypto/gui/decryptverifyoperationwidget.cpp b/src/crypto/gui/decryptverifyoperationwidget.cpp index 183193ba0..6735ce560 100644 --- a/src/crypto/gui/decryptverifyoperationwidget.cpp +++ b/src/crypto/gui/decryptverifyoperationwidget.cpp @@ -1,249 +1,249 @@ /* -*- mode: c++; c-basic-offset:4 -*- - uiserver/decryptverifyoperationwidget.cpp + crypto/gui/decryptverifyoperationwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "decryptverifyoperationwidget.h" #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Crypto::Gui; class DecryptVerifyOperationWidget::Private { friend class ::Kleo::Crypto::Gui::DecryptVerifyOperationWidget; DecryptVerifyOperationWidget *const q; public: explicit Private(DecryptVerifyOperationWidget *qq); ~Private(); void enableDisableWidgets() { const bool detached = ui.verifyDetachedCB.isChecked(); const bool archive = ui.archiveCB.isChecked(); ui.archiveCB.setEnabled(!detached); ui.archivesCB.setEnabled(archive && !detached); } private: struct UI { QGridLayout glay; QLabel inputLB; QStackedLayout inputStack; QLabel inputFileNameLB; FileNameRequester inputFileNameRQ; //------ QCheckBox verifyDetachedCB; //------ QLabel signedDataLB; QStackedLayout signedDataStack; QLabel signedDataFileNameLB; FileNameRequester signedDataFileNameRQ; //------ QHBoxLayout hlay; QCheckBox archiveCB; QComboBox archivesCB; explicit UI(DecryptVerifyOperationWidget *q); } ui; }; DecryptVerifyOperationWidget::Private::UI::UI(DecryptVerifyOperationWidget *q) : glay(q) , inputLB(i18n("Input file:"), q) , inputStack() , inputFileNameLB(q) , inputFileNameRQ(q) , verifyDetachedCB(i18n("&Input file is a detached signature"), q) , signedDataLB(i18n("&Signed data:"), q) , signedDataStack() , signedDataFileNameLB(q) , signedDataFileNameRQ(q) , hlay() , archiveCB(i18n("&Input file is an archive; unpack with:"), q) , archivesCB(q) { KDAB_SET_OBJECT_NAME(glay); KDAB_SET_OBJECT_NAME(inputLB); KDAB_SET_OBJECT_NAME(inputStack); KDAB_SET_OBJECT_NAME(inputFileNameLB); KDAB_SET_OBJECT_NAME(inputFileNameRQ); KDAB_SET_OBJECT_NAME(verifyDetachedCB); KDAB_SET_OBJECT_NAME(signedDataLB); KDAB_SET_OBJECT_NAME(signedDataStack); KDAB_SET_OBJECT_NAME(signedDataFileNameLB); KDAB_SET_OBJECT_NAME(signedDataFileNameRQ); KDAB_SET_OBJECT_NAME(hlay); KDAB_SET_OBJECT_NAME(archiveCB); KDAB_SET_OBJECT_NAME(archivesCB); inputStack.setContentsMargins(0, 0, 0, 0); signedDataStack.setContentsMargins(0, 0, 0, 0); signedDataLB.setEnabled(false); signedDataFileNameLB.setEnabled(false); signedDataFileNameRQ.setEnabled(false); archivesCB.setEnabled(false); glay.setContentsMargins(0, 0, 0, 0); glay.addWidget(&inputLB, 0, 0); glay.addLayout(&inputStack, 0, 1); inputStack.addWidget(&inputFileNameLB); inputStack.addWidget(&inputFileNameRQ); glay.addWidget(&verifyDetachedCB, 1, 0, 1, 2); glay.addWidget(&signedDataLB, 2, 0); glay.addLayout(&signedDataStack, 2, 1); signedDataStack.addWidget(&signedDataFileNameLB); signedDataStack.addWidget(&signedDataFileNameRQ); glay.addLayout(&hlay, 3, 0, 1, 2); hlay.addWidget(&archiveCB); hlay.addWidget(&archivesCB, 1); connect(&verifyDetachedCB, &QCheckBox::toggled, &signedDataLB, &QLabel::setEnabled); connect(&verifyDetachedCB, &QCheckBox::toggled, &signedDataFileNameLB, &QLabel::setEnabled); connect(&verifyDetachedCB, &QCheckBox::toggled, &signedDataFileNameRQ, &FileNameRequester::setEnabled); connect(&verifyDetachedCB, SIGNAL(toggled(bool)), q, SLOT(enableDisableWidgets())); connect(&archiveCB, SIGNAL(toggled(bool)), q, SLOT(enableDisableWidgets())); connect(&verifyDetachedCB, &QCheckBox::toggled, q, &DecryptVerifyOperationWidget::changed); connect(&inputFileNameRQ, &FileNameRequester::fileNameChanged, q, &DecryptVerifyOperationWidget::changed); connect(&signedDataFileNameRQ, &FileNameRequester::fileNameChanged, q, &DecryptVerifyOperationWidget::changed); } DecryptVerifyOperationWidget::Private::Private(DecryptVerifyOperationWidget *qq) : q(qq) , ui(q) { } DecryptVerifyOperationWidget::Private::~Private() { } DecryptVerifyOperationWidget::DecryptVerifyOperationWidget(QWidget *p) : QWidget(p) , d(new Private(this)) { setMode(DecryptVerifyOpaque); } DecryptVerifyOperationWidget::~DecryptVerifyOperationWidget() { } void DecryptVerifyOperationWidget::setArchiveDefinitions(const std::vector> &archiveDefinitions) { d->ui.archivesCB.clear(); for (const std::shared_ptr &ad : archiveDefinitions) { d->ui.archivesCB.addItem(ad->label(), QVariant::fromValue(ad)); } } void DecryptVerifyOperationWidget::setMode(Mode mode) { setMode(mode, std::shared_ptr()); } void DecryptVerifyOperationWidget::setMode(Mode mode, const std::shared_ptr &ad) { d->ui.verifyDetachedCB.setChecked(mode != DecryptVerifyOpaque); QWidget *inputWidget; QWidget *signedDataWidget; if (mode == VerifyDetachedWithSignedData) { inputWidget = &d->ui.inputFileNameRQ; signedDataWidget = &d->ui.signedDataFileNameLB; } else { inputWidget = &d->ui.inputFileNameLB; signedDataWidget = &d->ui.signedDataFileNameRQ; } d->ui.inputStack.setCurrentWidget(inputWidget); d->ui.signedDataStack.setCurrentWidget(signedDataWidget); d->ui.inputLB.setBuddy(inputWidget); d->ui.signedDataLB.setBuddy(signedDataWidget); d->ui.archiveCB.setChecked(ad.get() != nullptr); for (int i = 0, end = d->ui.archivesCB.count(); i != end; ++i) { if (ad == d->ui.archivesCB.itemData(i).value>()) { d->ui.archivesCB.setCurrentIndex(i); return; } } Q_EMIT changed(); } DecryptVerifyOperationWidget::Mode DecryptVerifyOperationWidget::mode() const { if (d->ui.verifyDetachedCB.isChecked()) if (d->ui.inputStack.currentIndex() == 0) { return VerifyDetachedWithSignature; } else { return VerifyDetachedWithSignedData; } else { return DecryptVerifyOpaque; } } void DecryptVerifyOperationWidget::setInputFileName(const QString &name) { d->ui.inputFileNameLB.setText(name); d->ui.inputFileNameRQ.setFileName(name); } QString DecryptVerifyOperationWidget::inputFileName() const { if (d->ui.inputStack.currentIndex() == 0) { return d->ui.inputFileNameLB.text(); } else { return d->ui.inputFileNameRQ.fileName(); } } void DecryptVerifyOperationWidget::setSignedDataFileName(const QString &name) { d->ui.signedDataFileNameLB.setText(name); d->ui.signedDataFileNameRQ.setFileName(name); } QString DecryptVerifyOperationWidget::signedDataFileName() const { if (d->ui.signedDataStack.currentIndex() == 0) { return d->ui.signedDataFileNameLB.text(); } else { return d->ui.signedDataFileNameRQ.fileName(); } } std::shared_ptr DecryptVerifyOperationWidget::selectedArchiveDefinition() const { if (mode() == DecryptVerifyOpaque && d->ui.archiveCB.isChecked()) { return d->ui.archivesCB.itemData(d->ui.archivesCB.currentIndex()).value>(); } else { return std::shared_ptr(); } } #include "moc_decryptverifyoperationwidget.cpp" diff --git a/src/crypto/signencryptfilescontroller.cpp b/src/crypto/signencryptfilescontroller.cpp index 55431be43..1515bddb4 100644 --- a/src/crypto/signencryptfilescontroller.cpp +++ b/src/crypto/signencryptfilescontroller.cpp @@ -1,783 +1,789 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/signencryptfilescontroller.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "signencryptfilescontroller.h" #include "signencrypttask.h" #include "crypto/gui/signencryptfileswizard.h" #include "crypto/taskcollection.h" #include "fileoperationspreferences.h" #include "utils/archivedefinition.h" #include "utils/input.h" #include "utils/kleo_assert.h" #include "utils/output.h" #include "utils/path-helper.h" #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Crypto; using namespace GpgME; class SignEncryptFilesController::Private { friend class ::Kleo::Crypto::SignEncryptFilesController; SignEncryptFilesController *const q; public: explicit Private(SignEncryptFilesController *qq); ~Private(); private: void slotWizardOperationPrepared(); void slotWizardCanceled(); private: void ensureWizardCreated(); void ensureWizardVisible(); void updateWizardMode(); void cancelAllTasks(); void reportError(int err, const QString &details) { q->setLastError(err, details); q->emitDoneOrError(); } void schedule(); std::shared_ptr takeRunnable(GpgME::Protocol proto); static void assertValidOperation(unsigned int); static QString titleForOperation(unsigned int op); private: std::vector> runnable, completed; std::shared_ptr cms, openpgp; QPointer wizard; QStringList files; unsigned int operation; Protocol protocol; }; SignEncryptFilesController::Private::Private(SignEncryptFilesController *qq) : q(qq) , runnable() , cms() , openpgp() , wizard() , files() , operation(SignAllowed | EncryptAllowed | ArchiveAllowed) , protocol(UnknownProtocol) { } SignEncryptFilesController::Private::~Private() { qCDebug(KLEOPATRA_LOG) << q << __func__; } QString SignEncryptFilesController::Private::titleForOperation(unsigned int op) { const bool signDisallowed = (op & SignMask) == SignDisallowed; const bool encryptDisallowed = (op & EncryptMask) == EncryptDisallowed; const bool archiveSelected = (op & ArchiveMask) == ArchiveForced; kleo_assert(!signDisallowed || !encryptDisallowed); if (!signDisallowed && encryptDisallowed) { if (archiveSelected) { return i18n("Archive and Sign Files"); } else { return i18n("Sign Files"); } } if (signDisallowed && !encryptDisallowed) { if (archiveSelected) { return i18n("Archive and Encrypt Files"); } else { return i18n("Encrypt Files"); } } if (archiveSelected) { return i18n("Archive and Sign/Encrypt Files"); } else { return i18n("Sign/Encrypt Files"); } } SignEncryptFilesController::SignEncryptFilesController(QObject *p) : Controller(p) , d(new Private(this)) { } SignEncryptFilesController::SignEncryptFilesController(const std::shared_ptr &ctx, QObject *p) : Controller(ctx, p) , d(new Private(this)) { } SignEncryptFilesController::~SignEncryptFilesController() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->wizard && !d->wizard->isVisible()) { delete d->wizard; } // d->wizard->close(); ### ? } void SignEncryptFilesController::setProtocol(Protocol proto) { kleo_assert(d->protocol == UnknownProtocol || d->protocol == proto); d->protocol = proto; d->ensureWizardCreated(); } Protocol SignEncryptFilesController::protocol() const { return d->protocol; } // static void SignEncryptFilesController::Private::assertValidOperation(unsigned int op) { kleo_assert((op & SignMask) == SignDisallowed || // (op & SignMask) == SignAllowed || // (op & SignMask) == SignSelected); kleo_assert((op & EncryptMask) == EncryptDisallowed || // (op & EncryptMask) == EncryptAllowed || // (op & EncryptMask) == EncryptSelected); kleo_assert((op & ArchiveMask) == ArchiveDisallowed || // (op & ArchiveMask) == ArchiveAllowed || // (op & ArchiveMask) == ArchiveForced); kleo_assert((op & ~(SignMask | EncryptMask | ArchiveMask)) == 0); } void SignEncryptFilesController::setOperationMode(unsigned int mode) { Private::assertValidOperation(mode); d->operation = mode; d->updateWizardMode(); } void SignEncryptFilesController::Private::updateWizardMode() { if (!wizard) { return; } wizard->setWindowTitle(titleForOperation(operation)); const unsigned int signOp = (operation & SignMask); const unsigned int encrOp = (operation & EncryptMask); const unsigned int archOp = (operation & ArchiveMask); if (signOp == SignDisallowed) { wizard->setSigningUserMutable(false); wizard->setSigningPreset(false); } else { wizard->setSigningUserMutable(true); wizard->setSigningPreset(signOp == SignSelected); } if (encrOp == EncryptDisallowed) { wizard->setEncryptionPreset(false); wizard->setEncryptionUserMutable(false); } else { wizard->setEncryptionUserMutable(true); wizard->setEncryptionPreset(encrOp == EncryptSelected); } wizard->setArchiveForced(archOp == ArchiveForced); wizard->setArchiveMutable(archOp == ArchiveAllowed); } unsigned int SignEncryptFilesController::operationMode() const { return d->operation; } static QString extension(bool pgp, bool sign, bool encrypt, bool ascii, bool detached) { unsigned int cls = pgp ? Class::OpenPGP : Class::CMS; if (encrypt) { cls |= Class::CipherText; } else if (sign) { cls |= detached ? Class::DetachedSignature : Class::OpaqueSignature; } cls |= ascii ? Class::Ascii : Class::Binary; const bool usePGPFileExt = FileOperationsPreferences().usePGPFileExt(); const auto ext = outputFileExtension(cls, usePGPFileExt); if (!ext.isEmpty()) { return ext; } else { return QStringLiteral("out"); } } static std::shared_ptr getDefaultAd() { const std::vector> ads = ArchiveDefinition::getArchiveDefinitions(); Q_ASSERT(!ads.empty()); std::shared_ptr ad = ads.front(); const FileOperationsPreferences prefs; const QString archiveCmd = prefs.archiveCommand(); auto it = std::find_if(ads.cbegin(), ads.cend(), [&archiveCmd](const std::shared_ptr &toCheck) { return toCheck->id() == archiveCmd; }); if (it != ads.cend()) { ad = *it; } return ad; } static QMap buildOutputNames(const QStringList &files, const bool archive) { QMap nameMap; // Build the default names for the wizard. QString baseNameCms; QString baseNamePgp; const QFileInfo firstFile(files.first()); if (archive) { QString baseName = files.size() > 1 ? i18nc("base name of an archive file, e.g. archive.zip or archive.tar.gz", "archive") : firstFile.baseName(); baseName = QDir(heuristicBaseDirectory(files)).absoluteFilePath(baseName); const auto ad = getDefaultAd(); baseNamePgp = baseName + QLatin1Char('.') + ad->extensions(GpgME::OpenPGP).first() + QLatin1Char('.'); baseNameCms = baseName + QLatin1Char('.') + ad->extensions(GpgME::CMS).first() + QLatin1Char('.'); } else { baseNameCms = baseNamePgp = files.first() + QLatin1Char('.'); } const FileOperationsPreferences prefs; const bool ascii = prefs.addASCIIArmor(); nameMap.insert(SignEncryptFilesWizard::SignatureCMS, baseNameCms + extension(false, true, false, ascii, true)); nameMap.insert(SignEncryptFilesWizard::EncryptedCMS, baseNameCms + extension(false, false, true, ascii, false)); nameMap.insert(SignEncryptFilesWizard::CombinedPGP, baseNamePgp + extension(true, true, true, ascii, false)); nameMap.insert(SignEncryptFilesWizard::EncryptedPGP, baseNamePgp + extension(true, false, true, ascii, false)); nameMap.insert(SignEncryptFilesWizard::SignaturePGP, baseNamePgp + extension(true, true, false, ascii, true)); nameMap.insert(SignEncryptFilesWizard::Directory, heuristicBaseDirectory(files)); return nameMap; } static QMap buildOutputNamesForDir(const QString &file, const QMap &orig) { QMap ret; const QString dir = orig.value(SignEncryptFilesWizard::Directory); if (dir.isEmpty()) { return orig; } // Build the default names for the wizard. const QFileInfo fi(file); const QString baseName = dir + QLatin1Char('/') + fi.fileName() + QLatin1Char('.'); const FileOperationsPreferences prefs; const bool ascii = prefs.addASCIIArmor(); ret.insert(SignEncryptFilesWizard::SignatureCMS, baseName + extension(false, true, false, ascii, true)); ret.insert(SignEncryptFilesWizard::EncryptedCMS, baseName + extension(false, false, true, ascii, false)); ret.insert(SignEncryptFilesWizard::CombinedPGP, baseName + extension(true, true, true, ascii, false)); ret.insert(SignEncryptFilesWizard::EncryptedPGP, baseName + extension(true, false, true, ascii, false)); ret.insert(SignEncryptFilesWizard::SignaturePGP, baseName + extension(true, true, false, ascii, true)); return ret; } // strips all trailing slashes from the filename, but keeps filename "/" static QString stripTrailingSlashes(const QString &fileName) { if (fileName.size() < 2 || !fileName.endsWith(QLatin1Char('/'))) { return fileName; } auto tmp = QStringView{fileName}.chopped(1); while (tmp.size() > 1 && tmp.endsWith(QLatin1Char('/'))) { tmp.chop(1); } return tmp.toString(); } static QStringList stripTrailingSlashesForAll(const QStringList &fileNames) { QStringList result; result.reserve(fileNames.size()); std::transform(fileNames.begin(), fileNames.end(), std::back_inserter(result), &stripTrailingSlashes); return result; } void SignEncryptFilesController::setFiles(const QStringList &files) { kleo_assert(!files.empty()); d->files = stripTrailingSlashesForAll(files); bool archive = false; if (d->files.size() > 1) { setOperationMode((operationMode() & ~ArchiveMask) | ArchiveAllowed); archive = true; } for (const auto &file : d->files) { if (QFileInfo(file).isDir()) { setOperationMode((operationMode() & ~ArchiveMask) | ArchiveForced); archive = true; break; } } d->ensureWizardCreated(); d->wizard->setSingleFile(!archive); d->wizard->setOutputNames(buildOutputNames(d->files, archive)); } void SignEncryptFilesController::Private::slotWizardCanceled() { qCDebug(KLEOPATRA_LOG) << q << __func__; q->cancel(); reportError(gpg_error(GPG_ERR_CANCELED), i18n("User cancel")); } void SignEncryptFilesController::start() { d->ensureWizardVisible(); } static std::shared_ptr createSignEncryptTaskForFileInfo(const QFileInfo &fi, bool ascii, const std::vector &recipients, const std::vector &signers, const QString &outputName, bool symmetric) { const std::shared_ptr task(new SignEncryptTask); Q_ASSERT(!signers.empty() || !recipients.empty() || symmetric); task->setAsciiArmor(ascii); if (!signers.empty()) { task->setSign(true); task->setSigners(signers); task->setDetachedSignature(true); } else { task->setSign(false); } if (!recipients.empty()) { task->setEncrypt(true); task->setRecipients(recipients); task->setDetachedSignature(false); } else { task->setEncrypt(false); } task->setEncryptSymmetric(symmetric); const QString input = fi.absoluteFilePath(); task->setInputFileName(input); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (task->protocol() != GpgME::OpenPGP) { + task->setInput(Input::createFromFile(input)); + } +#else task->setInput(Input::createFromFile(input)); +#endif task->setOutputFileName(outputName); return task; } static bool archiveJobsCanBeUsed([[maybe_unused]] GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::SignEncryptArchiveJob::isSupported(); } static std::shared_ptr createArchiveSignEncryptTaskForFiles(const QStringList &files, const std::shared_ptr &ad, bool pgp, bool ascii, const std::vector &recipients, const std::vector &signers, const QString &outputName, bool symmetric) { const std::shared_ptr task(new SignEncryptTask); task->setCreateArchive(true); task->setEncryptSymmetric(symmetric); Q_ASSERT(!signers.empty() || !recipients.empty() || symmetric); task->setAsciiArmor(ascii); if (!signers.empty()) { task->setSign(true); task->setSigners(signers); task->setDetachedSignature(false); } else { task->setSign(false); } if (!recipients.empty()) { task->setEncrypt(true); task->setRecipients(recipients); } else { task->setEncrypt(false); } const Protocol proto = pgp ? OpenPGP : CMS; task->setInputFileNames(files); if (!archiveJobsCanBeUsed(proto)) { // use legacy archive creation kleo_assert(ad); task->setInput(ad->createInputFromPackCommand(proto, files)); } task->setOutputFileName(outputName); return task; } static std::vector> createSignEncryptTasksForFileInfo(const QFileInfo &fi, bool ascii, const std::vector &pgpRecipients, const std::vector &pgpSigners, const std::vector &cmsRecipients, const std::vector &cmsSigners, const QMap &outputNames, bool symmetric) { std::vector> result; const bool pgp = !pgpSigners.empty() || !pgpRecipients.empty(); const bool cms = !cmsSigners.empty() || !cmsRecipients.empty(); result.reserve(pgp + cms); if (pgp || symmetric) { // Symmetric encryption is only supported for PGP int outKind = 0; if ((!pgpRecipients.empty() || symmetric) && !pgpSigners.empty()) { outKind = SignEncryptFilesWizard::CombinedPGP; } else if (!pgpRecipients.empty() || symmetric) { outKind = SignEncryptFilesWizard::EncryptedPGP; } else { outKind = SignEncryptFilesWizard::SignaturePGP; } result.push_back(createSignEncryptTaskForFileInfo(fi, ascii, pgpRecipients, pgpSigners, outputNames[outKind], symmetric)); } if (cms) { // There is no combined sign / encrypt in gpgsm so we create one sign task // and one encrypt task. Which leaves us with the age old dilemma, encrypt // then sign, or sign then encrypt. Ugly. if (!cmsSigners.empty()) { result.push_back( createSignEncryptTaskForFileInfo(fi, ascii, std::vector(), cmsSigners, outputNames[SignEncryptFilesWizard::SignatureCMS], false)); } if (!cmsRecipients.empty()) { result.push_back( createSignEncryptTaskForFileInfo(fi, ascii, cmsRecipients, std::vector(), outputNames[SignEncryptFilesWizard::EncryptedCMS], false)); } } return result; } static std::vector> createArchiveSignEncryptTasksForFiles(const QStringList &files, const std::shared_ptr &ad, bool ascii, const std::vector &pgpRecipients, const std::vector &pgpSigners, const std::vector &cmsRecipients, const std::vector &cmsSigners, const QMap &outputNames, bool symmetric) { std::vector> result; const bool pgp = !pgpSigners.empty() || !pgpRecipients.empty(); const bool cms = !cmsSigners.empty() || !cmsRecipients.empty(); result.reserve(pgp + cms); if (pgp || symmetric) { int outKind = 0; if ((!pgpRecipients.empty() || symmetric) && !pgpSigners.empty()) { outKind = SignEncryptFilesWizard::CombinedPGP; } else if (!pgpRecipients.empty() || symmetric) { outKind = SignEncryptFilesWizard::EncryptedPGP; } else { outKind = SignEncryptFilesWizard::SignaturePGP; } result.push_back(createArchiveSignEncryptTaskForFiles(files, ad, true, ascii, pgpRecipients, pgpSigners, outputNames[outKind], symmetric)); } if (cms) { if (!cmsSigners.empty()) { result.push_back(createArchiveSignEncryptTaskForFiles(files, ad, false, ascii, std::vector(), cmsSigners, outputNames[SignEncryptFilesWizard::SignatureCMS], false)); } if (!cmsRecipients.empty()) { result.push_back(createArchiveSignEncryptTaskForFiles(files, ad, false, ascii, cmsRecipients, std::vector(), outputNames[SignEncryptFilesWizard::EncryptedCMS], false)); } } return result; } namespace { static auto resolveFileNameConflicts(const std::vector> &tasks, QWidget *parent) { std::vector> resolvedTasks; OverwritePolicy overwritePolicy{parent, tasks.size() > 1 ? OverwritePolicy::MultipleFiles : OverwritePolicy::Options{}}; for (auto &task : tasks) { // by default, do not overwrite existing files task->setOverwritePolicy(std::make_shared(OverwritePolicy::Skip)); const auto outputFileName = task->outputFileName(); if (QFile::exists(outputFileName)) { const auto newFileName = overwritePolicy.obtainOverwritePermission(outputFileName); if (newFileName.isEmpty()) { if (overwritePolicy.policy() == OverwritePolicy::Cancel) { resolvedTasks.clear(); break; } // else Skip -> do not add task to the final task list continue; } else if (newFileName != outputFileName) { task->setOutputFileName(newFileName); } else { task->setOverwritePolicy(std::make_shared(OverwritePolicy::Overwrite)); } } resolvedTasks.push_back(task); } return resolvedTasks; } } void SignEncryptFilesController::Private::slotWizardOperationPrepared() { try { kleo_assert(wizard); kleo_assert(!files.empty()); const bool archive = ((wizard->outputNames().value(SignEncryptFilesWizard::Directory).isNull() && files.size() > 1) // || ((operation & ArchiveMask) == ArchiveForced)); const std::vector recipients = wizard->resolvedRecipients(); const std::vector signers = wizard->resolvedSigners(); const FileOperationsPreferences prefs; const bool ascii = prefs.addASCIIArmor(); std::vector pgpRecipients, cmsRecipients, pgpSigners, cmsSigners; for (const Key &k : recipients) { if (k.protocol() == GpgME::OpenPGP) { pgpRecipients.push_back(k); } else { cmsRecipients.push_back(k); } } for (const Key &k : signers) { if (k.protocol() == GpgME::OpenPGP) { pgpSigners.push_back(k); } else { cmsSigners.push_back(k); } } std::vector> tasks; if (!archive) { tasks.reserve(files.size()); } if (archive) { tasks = createArchiveSignEncryptTasksForFiles(files, getDefaultAd(), ascii, pgpRecipients, pgpSigners, cmsRecipients, cmsSigners, wizard->outputNames(), wizard->encryptSymmetric()); } else { for (const QString &file : std::as_const(files)) { const std::vector> created = createSignEncryptTasksForFileInfo(QFileInfo(file), ascii, pgpRecipients, pgpSigners, cmsRecipients, cmsSigners, buildOutputNamesForDir(file, wizard->outputNames()), wizard->encryptSymmetric()); tasks.insert(tasks.end(), created.begin(), created.end()); } } tasks = resolveFileNameConflicts(tasks, wizard); if (tasks.empty()) { q->cancel(); return; } kleo_assert(runnable.empty()); runnable.swap(tasks); for (const auto &task : std::as_const(runnable)) { q->connectTask(task); } std::shared_ptr coll(new TaskCollection); std::vector> tmp; std::copy(runnable.begin(), runnable.end(), std::back_inserter(tmp)); coll->setTasks(tmp); wizard->setTaskCollection(coll); QTimer::singleShot(0, q, SLOT(schedule())); } catch (const Kleo::Exception &e) { reportError(e.error().encodedError(), e.message()); } catch (const std::exception &e) { reportError( gpg_error(GPG_ERR_UNEXPECTED), i18n("Caught unexpected exception in SignEncryptFilesController::Private::slotWizardOperationPrepared: %1", QString::fromLocal8Bit(e.what()))); } catch (...) { reportError(gpg_error(GPG_ERR_UNEXPECTED), i18n("Caught unknown exception in SignEncryptFilesController::Private::slotWizardOperationPrepared")); } } void SignEncryptFilesController::Private::schedule() { if (!cms) if (const std::shared_ptr t = takeRunnable(CMS)) { t->start(); cms = t; } if (!openpgp) if (const std::shared_ptr t = takeRunnable(OpenPGP)) { t->start(); openpgp = t; } if (!cms && !openpgp) { kleo_assert(runnable.empty()); q->emitDoneOrError(); } } std::shared_ptr SignEncryptFilesController::Private::takeRunnable(GpgME::Protocol proto) { const auto it = std::find_if(runnable.begin(), runnable.end(), [proto](const std::shared_ptr &task) { return task->protocol() == proto; }); if (it == runnable.end()) { return std::shared_ptr(); } const std::shared_ptr result = *it; runnable.erase(it); return result; } void SignEncryptFilesController::doTaskDone(const Task *task, const std::shared_ptr &result) { Q_UNUSED(result) Q_ASSERT(task); // We could just delete the tasks here, but we can't use // Qt::QueuedConnection here (we need sender()) and other slots // might not yet have executed. Therefore, we push completed tasks // into a burial container if (task == d->cms.get()) { d->completed.push_back(d->cms); d->cms.reset(); } else if (task == d->openpgp.get()) { d->completed.push_back(d->openpgp); d->openpgp.reset(); } QTimer::singleShot(0, this, SLOT(schedule())); } void SignEncryptFilesController::cancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; try { if (d->wizard) { d->wizard->close(); } d->cancelAllTasks(); } catch (const std::exception &e) { qCDebug(KLEOPATRA_LOG) << "Caught exception: " << e.what(); } } void SignEncryptFilesController::Private::cancelAllTasks() { // we just kill all runnable tasks - this will not result in // signal emissions. runnable.clear(); // a cancel() will result in a call to if (cms) { cms->cancel(); } if (openpgp) { openpgp->cancel(); } } void SignEncryptFilesController::Private::ensureWizardCreated() { if (wizard) { return; } std::unique_ptr w(new SignEncryptFilesWizard); w->setAttribute(Qt::WA_DeleteOnClose); connect(w.get(), SIGNAL(operationPrepared()), q, SLOT(slotWizardOperationPrepared()), Qt::QueuedConnection); connect(w.get(), SIGNAL(rejected()), q, SLOT(slotWizardCanceled()), Qt::QueuedConnection); wizard = w.release(); updateWizardMode(); } void SignEncryptFilesController::Private::ensureWizardVisible() { ensureWizardCreated(); q->bringToForeground(wizard); } #include "moc_signencryptfilescontroller.cpp" diff --git a/src/crypto/signencrypttask.cpp b/src/crypto/signencrypttask.cpp index 685daf862..62ff33f71 100644 --- a/src/crypto/signencrypttask.cpp +++ b/src/crypto/signencrypttask.cpp @@ -1,908 +1,972 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/signencrypttask.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "signencrypttask.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include // for Qt::escape using namespace Kleo; using namespace Kleo::Crypto; using namespace GpgME; namespace { QString formatInputOutputLabel(const QString &input, const QString &output, bool outputDeleted) { return i18nc("Input file --> Output file (rarr is arrow", "%1 → %2", input.toHtmlEscaped(), outputDeleted ? QStringLiteral("%1").arg(output.toHtmlEscaped()) : output.toHtmlEscaped()); } class ErrorResult : public Task::Result { public: ErrorResult(bool sign, bool encrypt, const Error &err, const QString &errStr, const QString &input, const QString &output, const AuditLogEntry &auditLog) : Task::Result() , m_sign(sign) , m_encrypt(encrypt) , m_error(err) , m_errString(errStr) , m_inputLabel(input) , m_outputLabel(output) , m_auditLog(auditLog) { } QString overview() const override; QString details() const override; GpgME::Error error() const override { return m_error; } QString errorString() const override { return m_errString; } VisualCode code() const override { return NeutralError; } AuditLogEntry auditLog() const override { return m_auditLog; } private: const bool m_sign; const bool m_encrypt; const Error m_error; const QString m_errString; const QString m_inputLabel; const QString m_outputLabel; const AuditLogEntry m_auditLog; }; namespace { struct LabelAndError { QString label; QString errorString; }; } class SignEncryptFilesResult : public Task::Result { public: SignEncryptFilesResult(const SigningResult &sr, const LabelAndError &input, const LabelAndError &output, bool outputCreated, const AuditLogEntry &auditLog) : Task::Result() , m_sresult(sr) , m_input{input} , m_output{output} , m_outputCreated(outputCreated) , m_auditLog(auditLog) { qCDebug(KLEOPATRA_LOG) << "\ninputError :" << m_input.errorString << "\noutputError:" << m_output.errorString; Q_ASSERT(!m_sresult.isNull()); } SignEncryptFilesResult(const EncryptionResult &er, const LabelAndError &input, const LabelAndError &output, bool outputCreated, const AuditLogEntry &auditLog) : Task::Result() , m_eresult(er) , m_input{input} , m_output{output} , m_outputCreated(outputCreated) , m_auditLog(auditLog) { qCDebug(KLEOPATRA_LOG) << "\ninputError :" << m_input.errorString << "\noutputError:" << m_output.errorString; Q_ASSERT(!m_eresult.isNull()); } SignEncryptFilesResult(const SigningResult &sr, const EncryptionResult &er, const LabelAndError &input, const LabelAndError &output, bool outputCreated, const AuditLogEntry &auditLog) : Task::Result() , m_sresult(sr) , m_eresult(er) , m_input{input} , m_output{output} , m_outputCreated(outputCreated) , m_auditLog(auditLog) { qCDebug(KLEOPATRA_LOG) << "\ninputError :" << m_input.errorString << "\noutputError:" << m_output.errorString; Q_ASSERT(!m_sresult.isNull() || !m_eresult.isNull()); } QString overview() const override; QString details() const override; GpgME::Error error() const override; QString errorString() const override; VisualCode code() const override; AuditLogEntry auditLog() const override; private: const SigningResult m_sresult; const EncryptionResult m_eresult; const LabelAndError m_input; const LabelAndError m_output; const bool m_outputCreated; const AuditLogEntry m_auditLog; }; static QString makeSigningOverview(const Error &err) { if (err.isCanceled()) { return i18n("Signing canceled."); } if (err) { return i18n("Signing failed."); } return i18n("Signing succeeded."); } static QString makeResultOverview(const SigningResult &result) { return makeSigningOverview(result.error()); } static QString makeEncryptionOverview(const Error &err) { if (err.isCanceled()) { return i18n("Encryption canceled."); } if (err) { return i18n("Encryption failed."); } return i18n("Encryption succeeded."); } static QString makeResultOverview(const EncryptionResult &result) { return makeEncryptionOverview(result.error()); } static QString makeResultOverview(const SigningResult &sr, const EncryptionResult &er) { if (er.isNull() && sr.isNull()) { return QString(); } if (er.isNull()) { return makeResultOverview(sr); } if (sr.isNull()) { return makeResultOverview(er); } if (sr.error().isCanceled() || sr.error()) { return makeResultOverview(sr); } if (er.error().isCanceled() || er.error()) { return makeResultOverview(er); } return i18n("Signing and encryption succeeded."); } static QString escape(QString s) { s = s.toHtmlEscaped(); s.replace(QLatin1Char('\n'), QStringLiteral("
")); return s; } static QString makeResultDetails(const SigningResult &result, const QString &inputError, const QString &outputError) { const Error err = result.error(); if (err.code() == GPG_ERR_EIO) { if (!inputError.isEmpty()) { return i18n("Input error: %1", escape(inputError)); } else if (!outputError.isEmpty()) { return i18n("Output error: %1", escape(outputError)); } } if (err || err.isCanceled()) { return Formatting::errorAsString(err).toHtmlEscaped(); } return QString(); } static QString makeResultDetails(const EncryptionResult &result, const QString &inputError, const QString &outputError) { const Error err = result.error(); if (err.code() == GPG_ERR_EIO) { if (!inputError.isEmpty()) { return i18n("Input error: %1", escape(inputError)); } else if (!outputError.isEmpty()) { return i18n("Output error: %1", escape(outputError)); } } if (err || err.isCanceled()) { return Formatting::errorAsString(err).toHtmlEscaped(); } return i18n(" Encryption succeeded."); } } QString ErrorResult::overview() const { Q_ASSERT(m_error || m_error.isCanceled()); Q_ASSERT(m_sign || m_encrypt); const QString label = formatInputOutputLabel(m_inputLabel, m_outputLabel, true); const bool canceled = m_error.isCanceled(); if (m_sign && m_encrypt) { return canceled ? i18n("%1: Sign/encrypt canceled.", label) : i18n(" %1: Sign/encrypt failed.", label); } return i18nc("label: result. Example: foo -> foo.gpg: Encryption failed.", "%1: %2", label, m_sign ? makeSigningOverview(m_error) : makeEncryptionOverview(m_error)); } QString ErrorResult::details() const { return m_errString; } class SignEncryptTask::Private { friend class ::Kleo::Crypto::SignEncryptTask; SignEncryptTask *const q; public: explicit Private(SignEncryptTask *qq); private: QString inputLabel() const; QString outputLabel() const; bool removeExistingOutputFile(); void startSignEncryptJob(GpgME::Protocol proto); std::unique_ptr createSignJob(GpgME::Protocol proto); std::unique_ptr createSignEncryptJob(GpgME::Protocol proto); std::unique_ptr createEncryptJob(GpgME::Protocol proto); void startSignEncryptArchiveJob(GpgME::Protocol proto); std::unique_ptr createSignArchiveJob(GpgME::Protocol proto); std::unique_ptr createSignEncryptArchiveJob(GpgME::Protocol proto); std::unique_ptr createEncryptArchiveJob(GpgME::Protocol proto); std::shared_ptr makeErrorResult(const Error &err, const QString &errStr, const AuditLogEntry &auditLog); private: void slotResult(const SigningResult &); void slotResult(const SigningResult &, const EncryptionResult &); void slotResult(const EncryptionResult &); void slotResult(const QGpgME::Job *, const SigningResult &, const EncryptionResult &); private: std::shared_ptr input; std::shared_ptr output; QStringList inputFileNames; QString outputFileName; std::vector signers; std::vector recipients; bool sign : 1; bool encrypt : 1; bool detached : 1; bool symmetric : 1; bool clearsign : 1; bool archive : 1; QPointer job; QString labelText; std::shared_ptr m_overwritePolicy; }; SignEncryptTask::Private::Private(SignEncryptTask *qq) : q{qq} , sign{true} , encrypt{true} , detached{false} , clearsign{false} , archive{false} , m_overwritePolicy{new OverwritePolicy{OverwritePolicy::Ask}} { q->setAsciiArmor(true); } std::shared_ptr SignEncryptTask::Private::makeErrorResult(const Error &err, const QString &errStr, const AuditLogEntry &auditLog) { return std::shared_ptr(new ErrorResult(sign, encrypt, err, errStr, inputLabel(), outputLabel(), auditLog)); } SignEncryptTask::SignEncryptTask(QObject *p) : Task(p) , d(new Private(this)) { } SignEncryptTask::~SignEncryptTask() { } void SignEncryptTask::setInputFileName(const QString &fileName) { kleo_assert(!d->job); kleo_assert(!fileName.isEmpty()); d->inputFileNames = QStringList(fileName); } void SignEncryptTask::setInputFileNames(const QStringList &fileNames) { kleo_assert(!d->job); kleo_assert(!fileNames.empty()); d->inputFileNames = fileNames; } void SignEncryptTask::setInput(const std::shared_ptr &input) { kleo_assert(!d->job); kleo_assert(input); d->input = input; } void SignEncryptTask::setOutput(const std::shared_ptr &output) { kleo_assert(!d->job); kleo_assert(output); d->output = output; } void SignEncryptTask::setOutputFileName(const QString &fileName) { kleo_assert(!d->job); kleo_assert(!fileName.isEmpty()); d->outputFileName = fileName; } QString SignEncryptTask::outputFileName() const { return d->outputFileName; } void SignEncryptTask::setSigners(const std::vector &signers) { kleo_assert(!d->job); d->signers = signers; } void SignEncryptTask::setRecipients(const std::vector &recipients) { kleo_assert(!d->job); d->recipients = recipients; } void SignEncryptTask::setOverwritePolicy(const std::shared_ptr &policy) { kleo_assert(!d->job); d->m_overwritePolicy = policy; } void SignEncryptTask::setSign(bool sign) { kleo_assert(!d->job); d->sign = sign; } void SignEncryptTask::setEncrypt(bool encrypt) { kleo_assert(!d->job); d->encrypt = encrypt; } void SignEncryptTask::setDetachedSignature(bool detached) { kleo_assert(!d->job); d->detached = detached; } void SignEncryptTask::setEncryptSymmetric(bool symmetric) { kleo_assert(!d->job); d->symmetric = symmetric; } void SignEncryptTask::setClearsign(bool clearsign) { kleo_assert(!d->job); d->clearsign = clearsign; } void SignEncryptTask::setCreateArchive(bool archive) { kleo_assert(!d->job); d->archive = archive; } Protocol SignEncryptTask::protocol() const { if (d->sign && !d->signers.empty()) { return d->signers.front().protocol(); } if (d->encrypt || d->symmetric) { if (!d->recipients.empty()) { return d->recipients.front().protocol(); } else { return GpgME::OpenPGP; // symmetric OpenPGP encryption } } throw Kleo::Exception(gpg_error(GPG_ERR_INTERNAL), i18n("Cannot determine protocol for task")); } QString SignEncryptTask::label() const { if (!d->labelText.isEmpty()) { return d->labelText; } return d->inputLabel(); } QString SignEncryptTask::tag() const { return Formatting::displayName(protocol()); } unsigned long long SignEncryptTask::inputSize() const { return d->input ? d->input->size() : 0U; } static bool archiveJobsCanBeUsed(GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::SignEncryptArchiveJob::isSupported(); } void SignEncryptTask::doStart() { kleo_assert(!d->job); if (d->sign) { kleo_assert(!d->signers.empty()); if (d->archive) { kleo_assert(!d->detached && !d->clearsign); } } const auto proto = protocol(); if (d->archive && archiveJobsCanBeUsed(proto)) { d->startSignEncryptArchiveJob(proto); } else { - if (!d->output) { - d->output = Output::createFromFile(d->outputFileName, d->m_overwritePolicy); - } d->startSignEncryptJob(proto); } } QString SignEncryptTask::Private::inputLabel() const { if (input) { return input->label(); } if (!inputFileNames.empty()) { const auto firstFile = QFileInfo{inputFileNames.front()}.fileName(); return inputFileNames.size() == 1 ? firstFile : i18nc(", ...", "%1, ...", firstFile); } return {}; } QString SignEncryptTask::Private::outputLabel() const { return output ? output->label() : QFileInfo{outputFileName}.fileName(); } bool SignEncryptTask::Private::removeExistingOutputFile() { if (QFile::exists(outputFileName)) { bool fileRemoved = false; // we should already have asked the user for overwrite permission if (m_overwritePolicy && (m_overwritePolicy->policy() == OverwritePolicy::Overwrite)) { qCDebug(KLEOPATRA_LOG) << __func__ << "going to remove file for overwriting" << outputFileName; fileRemoved = QFile::remove(outputFileName); if (!fileRemoved) { qCDebug(KLEOPATRA_LOG) << __func__ << "removing file to overwrite failed"; } } else { qCDebug(KLEOPATRA_LOG) << __func__ << "we have no permission to overwrite" << outputFileName; } if (!fileRemoved) { QMetaObject::invokeMethod( q, [this]() { slotResult(nullptr, SigningResult{}, EncryptionResult{Error::fromCode(GPG_ERR_EEXIST)}); }, Qt::QueuedConnection); return false; } } return true; } void SignEncryptTask::Private::startSignEncryptJob(GpgME::Protocol proto) { +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (proto == GpgME::OpenPGP) { + kleo_assert(!input); + kleo_assert(!output); + } else { + kleo_assert(input); + + if (!output) { + output = Output::createFromFile(outputFileName, m_overwritePolicy); + } + } +#else kleo_assert(input); + if (!output) { + output = Output::createFromFile(outputFileName, m_overwritePolicy); + } +#endif + if (encrypt || symmetric) { Context::EncryptionFlags flags{Context::None}; if (proto == GpgME::OpenPGP) { flags = static_cast(flags | Context::AlwaysTrust); } if (symmetric) { flags = static_cast(flags | Context::Symmetric); qCDebug(KLEOPATRA_LOG) << "Adding symmetric flag"; } if (sign) { std::unique_ptr job = createSignEncryptJob(proto); kleo_assert(job.get()); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (proto == GpgME::OpenPGP) { + kleo_assert(inputFileNames.size() == 1); + job->setSigners(signers); + job->setRecipients(recipients); + job->setInputFile(inputFileNames.front()); + job->setOutputFile(outputFileName); + job->setEncryptionFlags(flags); + if (!removeExistingOutputFile()) { + return; + } + job->startIt(); + } else { + if (inputFileNames.size() == 1) { + job->setFileName(inputFileNames.front()); + } + job->start(signers, recipients, input->ioDevice(), output->ioDevice(), flags); + } +#else if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } - job->start(signers, recipients, input->ioDevice(), output->ioDevice(), flags); - +#endif this->job = job.release(); } else { std::unique_ptr job = createEncryptJob(proto); kleo_assert(job.get()); +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (proto == GpgME::OpenPGP) { + kleo_assert(inputFileNames.size() == 1); + job->setRecipients(recipients); + job->setInputFile(inputFileNames.front()); + job->setOutputFile(outputFileName); + job->setEncryptionFlags(flags); + if (!removeExistingOutputFile()) { + return; + } + job->startIt(); + } else { + if (inputFileNames.size() == 1) { + job->setFileName(inputFileNames.front()); + } + job->start(recipients, input->ioDevice(), output->ioDevice(), flags); + } +#else if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } - job->start(recipients, input->ioDevice(), output->ioDevice(), flags); - +#endif this->job = job.release(); } } else if (sign) { std::unique_ptr job = createSignJob(proto); kleo_assert(job.get()); kleo_assert(!(detached && clearsign)); - - job->start(signers, input->ioDevice(), output->ioDevice(), detached ? GpgME::Detached : clearsign ? GpgME::Clearsigned : GpgME::NormalSignatureMode); - + const GpgME::SignatureMode sigMode = detached ? GpgME::Detached : clearsign ? GpgME::Clearsigned : GpgME::NormalSignatureMode; +#if QGPGME_FILE_JOBS_SUPPORT_DIRECT_FILE_IO + if (proto == GpgME::OpenPGP) { + kleo_assert(inputFileNames.size() == 1); + job->setSigners(signers); + job->setInputFile(inputFileNames.front()); + job->setOutputFile(outputFileName); + job->setSigningFlags(sigMode); + if (!removeExistingOutputFile()) { + return; + } + job->startIt(); + } else { + job->start(signers, input->ioDevice(), output->ioDevice(), sigMode); + } +#else + job->start(signers, input->ioDevice(), output->ioDevice(), sigMode); +#endif this->job = job.release(); } else { kleo_assert(!"Either 'sign' or 'encrypt' or 'symmetric' must be set!"); } } void SignEncryptTask::cancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->job) { d->job->slotCancel(); } } std::unique_ptr SignEncryptTask::Private::createSignJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signJob(backend->signJob(q->asciiArmor(), /*textmode=*/false)); kleo_assert(signJob.get()); connect(signJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); connect(signJob.get(), SIGNAL(result(GpgME::SigningResult, QByteArray)), q, SLOT(slotResult(GpgME::SigningResult))); return signJob; } std::unique_ptr SignEncryptTask::Private::createSignEncryptJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signEncryptJob(backend->signEncryptJob(q->asciiArmor(), /*textmode=*/false)); kleo_assert(signEncryptJob.get()); connect(signEncryptJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); connect(signEncryptJob.get(), SIGNAL(result(GpgME::SigningResult, GpgME::EncryptionResult, QByteArray)), q, SLOT(slotResult(GpgME::SigningResult, GpgME::EncryptionResult))); return signEncryptJob; } std::unique_ptr SignEncryptTask::Private::createEncryptJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr encryptJob(backend->encryptJob(q->asciiArmor(), /*textmode=*/false)); kleo_assert(encryptJob.get()); connect(encryptJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); connect(encryptJob.get(), SIGNAL(result(GpgME::EncryptionResult, QByteArray)), q, SLOT(slotResult(GpgME::EncryptionResult))); return encryptJob; } void SignEncryptTask::Private::startSignEncryptArchiveJob(GpgME::Protocol proto) { kleo_assert(!input); kleo_assert(!output); #if !QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME output = Output::createFromFile(outputFileName, m_overwritePolicy); #endif const auto baseDirectory = heuristicBaseDirectory(inputFileNames); if (baseDirectory.isEmpty()) { throw Kleo::Exception(GPG_ERR_CONFLICT, i18n("Cannot find common base directory for these files:\n%1", inputFileNames.join(QLatin1Char('\n')))); } qCDebug(KLEOPATRA_LOG) << "heuristicBaseDirectory(" << inputFileNames << ") ->" << baseDirectory; const auto tempPaths = makeRelativeTo(baseDirectory, inputFileNames); const auto relativePaths = std::vector{tempPaths.begin(), tempPaths.end()}; qCDebug(KLEOPATRA_LOG) << "relative paths:" << relativePaths; if (encrypt || symmetric) { Context::EncryptionFlags flags{Context::None}; if (proto == GpgME::OpenPGP) { flags = static_cast(flags | Context::AlwaysTrust); } if (symmetric) { flags = static_cast(flags | Context::Symmetric); qCDebug(KLEOPATRA_LOG) << "Adding symmetric flag"; } if (sign) { labelText = i18nc("@info", "Creating signed and encrypted archive ..."); std::unique_ptr job = createSignEncryptArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); #if QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME job->setSigners(signers); job->setRecipients(recipients); job->setInputPaths(relativePaths); job->setOutputFile(outputFileName); job->setEncryptionFlags(flags); if (!removeExistingOutputFile()) { return; } job->startIt(); #else job->start(signers, recipients, relativePaths, output->ioDevice(), flags); #endif this->job = job.release(); } else { labelText = i18nc("@info", "Creating encrypted archive ..."); std::unique_ptr job = createEncryptArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); #if QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME job->setRecipients(recipients); job->setInputPaths(relativePaths); job->setOutputFile(outputFileName); job->setEncryptionFlags(flags); if (!removeExistingOutputFile()) { return; } job->startIt(); #else job->start(recipients, relativePaths, output->ioDevice(), flags); #endif this->job = job.release(); } } else if (sign) { labelText = i18nc("@info", "Creating signed archive ..."); std::unique_ptr job = createSignArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); #if QGPGME_ARCHIVE_JOBS_SUPPORT_OUTPUT_FILENAME job->setSigners(signers); job->setInputPaths(relativePaths); job->setOutputFile(outputFileName); if (!removeExistingOutputFile()) { return; } job->startIt(); #else job->start(signers, relativePaths, output->ioDevice()); #endif this->job = job.release(); } else { kleo_assert(!"Either 'sign' or 'encrypt' or 'symmetric' must be set!"); } } std::unique_ptr SignEncryptTask::Private::createSignArchiveJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signJob(backend->signArchiveJob(q->asciiArmor())); auto job = signJob.get(); kleo_assert(job); connect(job, &QGpgME::SignArchiveJob::dataProgress, q, &SignEncryptTask::setProgress); connect(job, &QGpgME::SignArchiveJob::result, q, [this, job](const GpgME::SigningResult &signResult) { slotResult(job, signResult, EncryptionResult{}); }); return signJob; } std::unique_ptr SignEncryptTask::Private::createSignEncryptArchiveJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr signEncryptJob(backend->signEncryptArchiveJob(q->asciiArmor())); auto job = signEncryptJob.get(); kleo_assert(job); connect(job, &QGpgME::SignEncryptArchiveJob::dataProgress, q, &SignEncryptTask::setProgress); connect(job, &QGpgME::SignEncryptArchiveJob::result, q, [this, job](const GpgME::SigningResult &signResult, const GpgME::EncryptionResult &encryptResult) { slotResult(job, signResult, encryptResult); }); return signEncryptJob; } std::unique_ptr SignEncryptTask::Private::createEncryptArchiveJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); std::unique_ptr encryptJob(backend->encryptArchiveJob(q->asciiArmor())); auto job = encryptJob.get(); kleo_assert(job); connect(job, &QGpgME::EncryptArchiveJob::dataProgress, q, &SignEncryptTask::setProgress); connect(job, &QGpgME::EncryptArchiveJob::result, q, [this, job](const GpgME::EncryptionResult &encryptResult) { slotResult(job, SigningResult{}, encryptResult); }); return encryptJob; } void SignEncryptTask::Private::slotResult(const SigningResult &result) { slotResult(qobject_cast(q->sender()), result, EncryptionResult{}); } void SignEncryptTask::Private::slotResult(const SigningResult &sresult, const EncryptionResult &eresult) { slotResult(qobject_cast(q->sender()), sresult, eresult); } void SignEncryptTask::Private::slotResult(const EncryptionResult &result) { slotResult(qobject_cast(q->sender()), SigningResult{}, result); } void SignEncryptTask::Private::slotResult(const QGpgME::Job *job, const SigningResult &sresult, const EncryptionResult &eresult) { qCDebug(KLEOPATRA_LOG) << q << __func__ << "job:" << job << "signing result:" << QGpgME::toLogString(sresult) << "encryption result:" << QGpgME::toLogString(eresult); const AuditLogEntry auditLog = AuditLogEntry::fromJob(job); bool outputCreated = false; if (input && input->failed()) { if (output) { output->cancel(); } q->emitResult(makeErrorResult(Error::fromCode(GPG_ERR_EIO), i18n("Input error: %1", escape(input->errorString())), auditLog)); return; } else if (sresult.error().code() || eresult.error().code()) { if (output) { output->cancel(); } if (!outputFileName.isEmpty() && eresult.error().code() != GPG_ERR_EEXIST) { // ensure that the output file is removed if the task was canceled or an error occurred; // unless a "file exists" error occurred because this means that the file with the name // of outputFileName wasn't created as result of this task if (QFile::exists(outputFileName)) { qCDebug(KLEOPATRA_LOG) << __func__ << "Removing output file" << outputFileName << "after error or cancel"; if (!QFile::remove(outputFileName)) { qCDebug(KLEOPATRA_LOG) << __func__ << "Removing output file" << outputFileName << "failed"; } } } } else { try { kleo_assert(!sresult.isNull() || !eresult.isNull()); if (output) { output->finalize(); } outputCreated = true; if (input) { input->finalize(); } } catch (const GpgME::Exception &e) { q->emitResult(makeErrorResult(e.error(), QString::fromLocal8Bit(e.what()), auditLog)); return; } } const LabelAndError inputInfo{inputLabel(), input ? input->errorString() : QString{}}; const LabelAndError outputInfo{outputLabel(), output ? output->errorString() : QString{}}; q->emitResult(std::shared_ptr(new SignEncryptFilesResult(sresult, eresult, inputInfo, outputInfo, outputCreated, auditLog))); } QString SignEncryptFilesResult::overview() const { const QString files = formatInputOutputLabel(m_input.label, m_output.label, !m_outputCreated); return files + QLatin1String(": ") + makeOverview(makeResultOverview(m_sresult, m_eresult)); } QString SignEncryptFilesResult::details() const { return errorString(); } GpgME::Error SignEncryptFilesResult::error() const { if (m_sresult.error().code()) { return m_sresult.error(); } if (m_eresult.error().code()) { return m_eresult.error(); } return {}; } QString SignEncryptFilesResult::errorString() const { const bool sign = !m_sresult.isNull(); const bool encrypt = !m_eresult.isNull(); kleo_assert(sign || encrypt); if (sign && encrypt) { return m_sresult.error().code() ? makeResultDetails(m_sresult, m_input.errorString, m_output.errorString) : m_eresult.error().code() ? makeResultDetails(m_eresult, m_input.errorString, m_output.errorString) : QString(); } return sign ? makeResultDetails(m_sresult, m_input.errorString, m_output.errorString) // : makeResultDetails(m_eresult, m_input.errorString, m_output.errorString); } Task::Result::VisualCode SignEncryptFilesResult::code() const { if (m_sresult.error().isCanceled() || m_eresult.error().isCanceled()) { return Warning; } return (m_sresult.error().code() || m_eresult.error().code()) ? NeutralError : NeutralSuccess; } AuditLogEntry SignEncryptFilesResult::auditLog() const { return m_auditLog; } #include "moc_signencrypttask.cpp" diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp index 1eac494ad..af7ded56e 100644 --- a/src/dialogs/certificatedetailswidget.cpp +++ b/src/dialogs/certificatedetailswidget.cpp @@ -1,1132 +1,1136 @@ /* dialogs/certificatedetailswidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-FileCopyrightText: 2022 Felix Tiede SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certificatedetailswidget.h" #include "exportdialog.h" #include "kleopatra_debug.h" #include "subkeyswidget.h" #include "trustchainwidget.h" #include "weboftrustdialog.h" #include "commands/certifycertificatecommand.h" #include "commands/changeexpirycommand.h" #include "commands/changepassphrasecommand.h" #ifdef MAILAKONADI_ENABLED #include "commands/exportopenpgpcerttoprovidercommand.h" #endif // MAILAKONADI_ENABLED #include "commands/adduseridcommand.h" #include "commands/detailscommand.h" #include "commands/dumpcertificatecommand.h" #include "commands/genrevokecommand.h" #include "commands/refreshcertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "commands/revokeuseridcommand.h" #include "commands/setprimaryuseridcommand.h" #include "utils/accessibility.h" #include "utils/tags.h" #include "view/infofield.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 #if __has_include() #include #if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L #define USE_RANGES #endif #endif #include Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; namespace { std::vector selectedUserIDs(const QTreeWidget *treeWidget) { if (!treeWidget) { return {}; } std::vector userIDs; const auto selected = treeWidget->selectedItems(); std::transform(selected.begin(), selected.end(), std::back_inserter(userIDs), [](const QTreeWidgetItem *item) { return item->data(0, Qt::UserRole).value(); }); return userIDs; } } class CertificateDetailsWidget::Private { public: Private(CertificateDetailsWidget *qq); void setupCommonProperties(); void updateUserIDActions(); void setUpUserIDTable(); void setUpSMIMEAdressList(); void setupPGPProperties(); void setupSMIMEProperties(); void revokeUserID(const GpgME::UserID &uid); void revokeSelectedUserID(); void genRevokeCert(); void refreshCertificate(); void certifyUserIDs(); void revokeCertifications(); void webOfTrustClicked(); void exportClicked(); void addUserID(); void setPrimaryUserID(const GpgME::UserID &uid = {}); void changePassphrase(); void changeExpiration(); void keysMayHaveChanged(); void showTrustChainDialog(); void showMoreDetails(); void userIDTableContextMenuRequested(const QPoint &p); QString tofuTooltipString(const GpgME::UserID &uid) const; QIcon trustLevelIcon(const GpgME::UserID &uid) const; QString trustLevelText(const GpgME::UserID &uid) const; void showIssuerCertificate(); void updateKey(); void setUpdatedKey(const GpgME::Key &key); void keyListDone(const GpgME::KeyListResult &, const std::vector &, const QString &, const GpgME::Error &); void copyFingerprintToClipboard(); private: CertificateDetailsWidget *const q; public: GpgME::Key key; bool updateInProgress = false; private: InfoField *attributeField(const QString &attributeName) { const auto keyValuePairIt = ui.smimeAttributeFields.find(attributeName); if (keyValuePairIt != ui.smimeAttributeFields.end()) { return (*keyValuePairIt).second.get(); } return nullptr; } private: struct UI { QWidget *userIDs = nullptr; QLabel *userIDTableLabel = nullptr; NavigatableTreeWidget *userIDTable = nullptr; QPushButton *addUserIDBtn = nullptr; QPushButton *setPrimaryUserIDBtn = nullptr; QPushButton *certifyBtn = nullptr; QPushButton *revokeCertificationsBtn = nullptr; QPushButton *revokeUserIDBtn = nullptr; QPushButton *webOfTrustBtn = nullptr; std::map> smimeAttributeFields; std::unique_ptr smimeTrustLevelField; std::unique_ptr validFromField; std::unique_ptr expiresField; QAction *changeExpirationAction = nullptr; std::unique_ptr fingerprintField; QAction *copyFingerprintAction = nullptr; std::unique_ptr smimeIssuerField; QAction *showIssuerCertificateAction = nullptr; std::unique_ptr complianceField; std::unique_ptr trustedIntroducerField; QLabel *smimeRelatedAddresses = nullptr; QListWidget *smimeAddressList = nullptr; QPushButton *moreDetailsBtn = nullptr; QPushButton *trustChainDetailsBtn = nullptr; QPushButton *refreshBtn = nullptr; QPushButton *changePassphraseBtn = nullptr; QPushButton *exportBtn = nullptr; QPushButton *genRevokeBtn = nullptr; void setupUi(QWidget *parent) { auto mainLayout = new QVBoxLayout{parent}; userIDs = new QWidget{parent}; { auto userIDsLayout = new QVBoxLayout{userIDs}; userIDsLayout->setContentsMargins({}); userIDTableLabel = new QLabel(i18n("User IDs:"), parent); userIDsLayout->addWidget(userIDTableLabel); userIDTable = new NavigatableTreeWidget{parent}; userIDTableLabel->setBuddy(userIDTable); userIDTable->setAccessibleName(i18n("User IDs")); QTreeWidgetItem *__qtreewidgetitem = new QTreeWidgetItem(); __qtreewidgetitem->setText(0, QString::fromUtf8("1")); userIDTable->setHeaderItem(__qtreewidgetitem); userIDTable->setEditTriggers(QAbstractItemView::NoEditTriggers); userIDTable->setSelectionMode(QAbstractItemView::ExtendedSelection); userIDTable->setRootIsDecorated(false); userIDTable->setUniformRowHeights(true); userIDTable->setAllColumnsShowFocus(false); userIDsLayout->addWidget(userIDTable); { auto buttonRow = new QHBoxLayout; addUserIDBtn = new QPushButton(i18nc("@action:button", "Add User ID"), parent); buttonRow->addWidget(addUserIDBtn); setPrimaryUserIDBtn = new QPushButton{i18nc("@action:button", "Flag as Primary"), parent}; setPrimaryUserIDBtn->setToolTip(i18nc("@info:tooltip", "Flag the selected user ID as the primary user ID of this key.")); buttonRow->addWidget(setPrimaryUserIDBtn); certifyBtn = new QPushButton(i18nc("@action:button", "Certify User IDs"), parent); buttonRow->addWidget(certifyBtn); webOfTrustBtn = new QPushButton(i18nc("@action:button", "Show Certifications"), parent); buttonRow->addWidget(webOfTrustBtn); revokeCertificationsBtn = new QPushButton(i18nc("@action:button", "Revoke Certifications"), parent); buttonRow->addWidget(revokeCertificationsBtn); revokeUserIDBtn = new QPushButton(i18nc("@action:button", "Revoke User ID"), parent); buttonRow->addWidget(revokeUserIDBtn); buttonRow->addStretch(1); userIDsLayout->addLayout(buttonRow); } userIDsLayout->addWidget(new KSeparator{Qt::Horizontal, parent}); } mainLayout->addWidget(userIDs); { auto gridLayout = new QGridLayout; gridLayout->setColumnStretch(1, 1); int row = -1; for (const auto &attribute : DN::attributeOrder()) { const auto attributeLabel = DN::attributeNameToLabel(attribute); if (attributeLabel.isEmpty()) { continue; } const auto labelWithColon = i18nc("interpunctation for labels", "%1:", attributeLabel); const auto &[it, inserted] = smimeAttributeFields.try_emplace(attribute, std::make_unique(labelWithColon, parent)); if (inserted) { row++; const auto &field = it->second; gridLayout->addWidget(field->label(), row, 0); gridLayout->addLayout(field->layout(), row, 1); } } row++; smimeTrustLevelField = std::make_unique(i18n("Trust level:"), parent); gridLayout->addWidget(smimeTrustLevelField->label(), row, 0); gridLayout->addLayout(smimeTrustLevelField->layout(), row, 1); row++; validFromField = std::make_unique(i18n("Valid from:"), parent); gridLayout->addWidget(validFromField->label(), row, 0); gridLayout->addLayout(validFromField->layout(), row, 1); row++; expiresField = std::make_unique(i18n("Valid until:"), parent); changeExpirationAction = new QAction{parent}; changeExpirationAction->setIcon(QIcon::fromTheme(QStringLiteral("editor"))); changeExpirationAction->setToolTip(i18nc("@info:tooltip", "Change the end of the validity period")); Kleo::setAccessibleName(changeExpirationAction, i18nc("@action:button", "Change Validity")); expiresField->setAction(changeExpirationAction); gridLayout->addWidget(expiresField->label(), row, 0); gridLayout->addLayout(expiresField->layout(), row, 1); row++; fingerprintField = std::make_unique(i18n("Fingerprint:"), parent); if (QGuiApplication::clipboard()) { copyFingerprintAction = new QAction{parent}; copyFingerprintAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); copyFingerprintAction->setToolTip(i18nc("@info:tooltip", "Copy the fingerprint to the clipboard")); Kleo::setAccessibleName(copyFingerprintAction, i18nc("@action:button", "Copy fingerprint")); fingerprintField->setAction(copyFingerprintAction); } gridLayout->addWidget(fingerprintField->label(), row, 0); gridLayout->addLayout(fingerprintField->layout(), row, 1); row++; smimeIssuerField = std::make_unique(i18n("Issuer:"), parent); showIssuerCertificateAction = new QAction{parent}; showIssuerCertificateAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); showIssuerCertificateAction->setToolTip(i18nc("@info:tooltip", "Show the issuer certificate")); Kleo::setAccessibleName(showIssuerCertificateAction, i18nc("@action:button", "Show certificate")); smimeIssuerField->setAction(showIssuerCertificateAction); gridLayout->addWidget(smimeIssuerField->label(), row, 0); gridLayout->addLayout(smimeIssuerField->layout(), row, 1); row++; complianceField = std::make_unique(i18n("Compliance:"), parent); gridLayout->addWidget(complianceField->label(), row, 0); gridLayout->addLayout(complianceField->layout(), row, 1); row++; trustedIntroducerField = std::make_unique(i18n("Trusted introducer for:"), parent); gridLayout->addWidget(trustedIntroducerField->label(), row, 0); trustedIntroducerField->setToolTip(i18n("See certifications for details.")); gridLayout->addLayout(trustedIntroducerField->layout(), row, 1); mainLayout->addLayout(gridLayout); } smimeRelatedAddresses = new QLabel(i18n("Related addresses:"), parent); mainLayout->addWidget(smimeRelatedAddresses); smimeAddressList = new QListWidget{parent}; smimeRelatedAddresses->setBuddy(smimeAddressList); smimeAddressList->setAccessibleName(i18n("Related addresses")); smimeAddressList->setEditTriggers(QAbstractItemView::NoEditTriggers); smimeAddressList->setSelectionMode(QAbstractItemView::SingleSelection); mainLayout->addWidget(smimeAddressList); mainLayout->addStretch(); { auto buttonRow = new QHBoxLayout; moreDetailsBtn = new QPushButton(i18nc("@action:button", "More Details..."), parent); buttonRow->addWidget(moreDetailsBtn); trustChainDetailsBtn = new QPushButton(i18nc("@action:button", "Trust Chain Details"), parent); buttonRow->addWidget(trustChainDetailsBtn); refreshBtn = new QPushButton{i18nc("@action:button", "Update"), parent}; buttonRow->addWidget(refreshBtn); exportBtn = new QPushButton(i18nc("@action:button", "Export"), parent); buttonRow->addWidget(exportBtn); changePassphraseBtn = new QPushButton(i18nc("@action:button", "Change Passphrase"), parent); buttonRow->addWidget(changePassphraseBtn); genRevokeBtn = new QPushButton(i18nc("@action:button", "Generate Revocation Certificate"), parent); genRevokeBtn->setToolTip(u"" % i18n("A revocation certificate is a file that serves as a \"kill switch\" to publicly " "declare that a key shall not anymore be used. It is not possible " "to retract such a revocation certificate once it has been published.") % u""); buttonRow->addWidget(genRevokeBtn); buttonRow->addStretch(1); mainLayout->addLayout(buttonRow); } } } ui; }; CertificateDetailsWidget::Private::Private(CertificateDetailsWidget *qq) : q{qq} { ui.setupUi(q); ui.userIDTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui.userIDTable, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) { userIDTableContextMenuRequested(p); }); connect(ui.userIDTable, &QTreeWidget::itemSelectionChanged, q, [this]() { updateUserIDActions(); }); connect(ui.addUserIDBtn, &QPushButton::clicked, q, [this]() { addUserID(); }); connect(ui.setPrimaryUserIDBtn, &QPushButton::clicked, q, [this]() { setPrimaryUserID(); }); connect(ui.revokeUserIDBtn, &QPushButton::clicked, q, [this]() { revokeSelectedUserID(); }); connect(ui.changePassphraseBtn, &QPushButton::clicked, q, [this]() { changePassphrase(); }); connect(ui.genRevokeBtn, &QPushButton::clicked, q, [this]() { genRevokeCert(); }); connect(ui.changeExpirationAction, &QAction::triggered, q, [this]() { changeExpiration(); }); connect(ui.showIssuerCertificateAction, &QAction::triggered, q, [this]() { showIssuerCertificate(); }); connect(ui.trustChainDetailsBtn, &QPushButton::pressed, q, [this]() { showTrustChainDialog(); }); connect(ui.moreDetailsBtn, &QPushButton::pressed, q, [this]() { showMoreDetails(); }); connect(ui.refreshBtn, &QPushButton::clicked, q, [this]() { refreshCertificate(); }); connect(ui.certifyBtn, &QPushButton::clicked, q, [this]() { certifyUserIDs(); }); connect(ui.revokeCertificationsBtn, &QPushButton::clicked, q, [this]() { revokeCertifications(); }); connect(ui.webOfTrustBtn, &QPushButton::clicked, q, [this]() { webOfTrustClicked(); }); connect(ui.exportBtn, &QPushButton::clicked, q, [this]() { exportClicked(); }); if (ui.copyFingerprintAction) { connect(ui.copyFingerprintAction, &QAction::triggered, q, [this]() { copyFingerprintToClipboard(); }); } connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { keysMayHaveChanged(); }); } void CertificateDetailsWidget::Private::setupCommonProperties() { const bool isOpenPGP = key.protocol() == GpgME::OpenPGP; const bool isSMIME = key.protocol() == GpgME::CMS; const bool isOwnKey = key.hasSecret(); const auto isLocalKey = !isRemoteKey(key); const auto keyCanBeCertified = Kleo::canBeCertified(key); // update visibility of UI elements ui.userIDs->setVisible(isOpenPGP); ui.addUserIDBtn->setVisible(isOwnKey); ui.setPrimaryUserIDBtn->setVisible(isOwnKey); // ui.certifyBtn->setVisible(true); // always visible (for OpenPGP keys) // ui.webOfTrustBtn->setVisible(true); // always visible (for OpenPGP keys) ui.revokeCertificationsBtn->setVisible(Kleo::Commands::RevokeCertificationCommand::isSupported()); ui.revokeUserIDBtn->setVisible(isOwnKey); for (const auto &[_, field] : ui.smimeAttributeFields) { field->setVisible(isSMIME); } ui.smimeTrustLevelField->setVisible(isSMIME); // ui.validFromField->setVisible(true); // always visible // ui.expiresField->setVisible(true); // always visible if (isOpenPGP && isOwnKey) { ui.expiresField->setAction(ui.changeExpirationAction); } else { ui.expiresField->setAction(nullptr); } // ui.fingerprintField->setVisible(true); // always visible ui.smimeIssuerField->setVisible(isSMIME); ui.complianceField->setVisible(DeVSCompliance::isCompliant()); ui.trustedIntroducerField->setVisible(isOpenPGP); // may be hidden again by setupPGPProperties() ui.smimeRelatedAddresses->setVisible(isSMIME); ui.smimeAddressList->setVisible(isSMIME); ui.moreDetailsBtn->setVisible(isLocalKey); + ui.moreDetailsBtn->setText(isSMIME ? i18nc("@action:button", "More Details...") + : isOwnKey ? i18nc("@action:button", "Manage Subkeys") + : i18nc("@action:button", "Show Subkeys")); + ui.trustChainDetailsBtn->setVisible(isSMIME); ui.refreshBtn->setVisible(isLocalKey); ui.changePassphraseBtn->setVisible(isSecretKeyStoredInKeyRing(key)); ui.exportBtn->setVisible(isLocalKey); ui.genRevokeBtn->setVisible(isOpenPGP && isOwnKey); // update availability of buttons const auto userCanSignUserIDs = userHasCertificationKey(); ui.addUserIDBtn->setEnabled(canBeUsedForSecretKeyOperations(key)); ui.setPrimaryUserIDBtn->setEnabled(false); // requires a selected user ID ui.certifyBtn->setEnabled(isLocalKey && keyCanBeCertified && userCanSignUserIDs); ui.webOfTrustBtn->setEnabled(isLocalKey); ui.revokeCertificationsBtn->setEnabled(userCanSignUserIDs && isLocalKey); ui.revokeUserIDBtn->setEnabled(false); // requires a selected user ID ui.changeExpirationAction->setEnabled(canBeUsedForSecretKeyOperations(key)); ui.changePassphraseBtn->setEnabled(isSecretKeyStoredInKeyRing(key)); ui.genRevokeBtn->setEnabled(canBeUsedForSecretKeyOperations(key)); // update values of protocol-independent UI elements ui.validFromField->setValue(Formatting::creationDateString(key), Formatting::accessibleCreationDate(key)); ui.expiresField->setValue(Formatting::expirationDateString(key, i18nc("Valid until:", "unlimited")), Formatting::accessibleExpirationDate(key)); ui.fingerprintField->setValue(Formatting::prettyID(key.primaryFingerprint()), Formatting::accessibleHexID(key.primaryFingerprint())); if (DeVSCompliance::isCompliant()) { ui.complianceField->setValue(Kleo::Formatting::complianceStringForKey(key)); } } void CertificateDetailsWidget::Private::updateUserIDActions() { const auto userIDs = selectedUserIDs(ui.userIDTable); const auto singleUserID = userIDs.size() == 1 ? userIDs.front() : GpgME::UserID{}; const bool isPrimaryUserID = !singleUserID.isNull() && (ui.userIDTable->selectedItems().front() == ui.userIDTable->topLevelItem(0)); ui.setPrimaryUserIDBtn->setEnabled(!singleUserID.isNull() // && !isPrimaryUserID // && !Kleo::isRevokedOrExpired(singleUserID) // && canBeUsedForSecretKeyOperations(key)); ui.revokeUserIDBtn->setEnabled(!singleUserID.isNull() && canCreateCertifications(key) && canRevokeUserID(singleUserID)); } void CertificateDetailsWidget::Private::setUpUserIDTable() { ui.userIDTable->clear(); QStringList headers = {i18n("Email"), i18n("Name"), i18n("Trust Level"), i18n("Tags")}; ui.userIDTable->setColumnCount(headers.count()); ui.userIDTable->setColumnWidth(0, 200); ui.userIDTable->setColumnWidth(1, 200); ui.userIDTable->setHeaderLabels(headers); const auto uids = key.userIDs(); for (unsigned int i = 0; i < uids.size(); ++i) { const auto &uid = uids[i]; auto item = new QTreeWidgetItem; const QString toolTip = tofuTooltipString(uid); item->setData(0, Qt::UserRole, QVariant::fromValue(uid)); auto pMail = Kleo::Formatting::prettyEMail(uid); auto pName = Kleo::Formatting::prettyName(uid); item->setData(0, Qt::DisplayRole, pMail); item->setData(0, Qt::ToolTipRole, toolTip); item->setData(0, Qt::AccessibleTextRole, pMail.isEmpty() ? i18nc("text for screen readers for an empty email address", "no email") : pMail); item->setData(1, Qt::DisplayRole, pName); item->setData(1, Qt::ToolTipRole, toolTip); item->setData(2, Qt::DecorationRole, trustLevelIcon(uid)); item->setData(2, Qt::DisplayRole, trustLevelText(uid)); item->setData(2, Qt::ToolTipRole, toolTip); GpgME::Error err; QStringList tagList; for (const auto &tag : uid.remarks(Tags::tagKeys(), err)) { if (err) { qCWarning(KLEOPATRA_LOG) << "Getting remarks for user ID" << uid.id() << "failed:" << err; } tagList << QString::fromStdString(tag); } qCDebug(KLEOPATRA_LOG) << "tagList:" << tagList; const auto tags = tagList.join(QStringLiteral("; ")); item->setData(3, Qt::DisplayRole, tags); item->setData(3, Qt::ToolTipRole, toolTip); ui.userIDTable->addTopLevelItem(item); } if (!Tags::tagsEnabled()) { ui.userIDTable->hideColumn(3); } } void CertificateDetailsWidget::Private::setUpSMIMEAdressList() { ui.smimeAddressList->clear(); const auto *const emailField = attributeField(QStringLiteral("EMAIL")); // add email address from primary user ID if not listed already as attribute field if (!emailField) { const auto ownerId = key.userID(0); const Kleo::DN dn(ownerId.id()); const QString dnEmail = dn[QStringLiteral("EMAIL")]; if (!dnEmail.isEmpty()) { ui.smimeAddressList->addItem(dnEmail); } } if (key.numUserIDs() > 1) { // iterate over the secondary user IDs #ifdef USE_RANGES for (const auto uids = key.userIDs(); const auto &uid : std::ranges::subrange(std::next(uids.begin()), uids.end())) { #else const auto uids = key.userIDs(); for (auto it = std::next(uids.begin()); it != uids.end(); ++it) { const auto &uid = *it; #endif const auto name = Kleo::Formatting::prettyName(uid); const auto email = Kleo::Formatting::prettyEMail(uid); QString itemText; if (name.isEmpty() && !email.isEmpty()) { // skip email addresses already listed in email attribute field if (emailField && email == emailField->value()) { continue; } itemText = email; } else { // S/MIME certificates sometimes contain urls where both // name and mail is empty. In that case we print whatever // the uid is as name. // // Can be ugly like (3:uri24:http://ca.intevation.org), but // this is better then showing an empty entry. itemText = QString::fromUtf8(uid.id()); } // avoid duplicate entries in the list if (ui.smimeAddressList->findItems(itemText, Qt::MatchExactly).empty()) { ui.smimeAddressList->addItem(itemText); } } } if (ui.smimeAddressList->count() == 0) { ui.smimeRelatedAddresses->setVisible(false); ui.smimeAddressList->setVisible(false); } } void CertificateDetailsWidget::Private::revokeUserID(const GpgME::UserID &userId) { const QString message = xi18nc("@info", "Do you really want to revoke the user ID%1 ?", QString::fromUtf8(userId.id())); auto confirmButton = KStandardGuiItem::ok(); confirmButton.setText(i18nc("@action:button", "Revoke User ID")); confirmButton.setToolTip({}); const auto choice = KMessageBox::questionTwoActions(q->window(), message, i18nc("@title:window", "Confirm Revocation"), confirmButton, KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::WindowModal); if (choice != KMessageBox::ButtonCode::PrimaryAction) { return; } auto cmd = new Commands::RevokeUserIDCommand(userId); cmd->setParentWidget(q); connect(cmd, &Command::finished, q, [this]() { ui.userIDTable->setEnabled(true); // the Revoke User ID button will be updated by the key update updateKey(); }); ui.userIDTable->setEnabled(false); ui.revokeUserIDBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::revokeSelectedUserID() { const auto userIDs = selectedUserIDs(ui.userIDTable); if (userIDs.size() != 1) { return; } revokeUserID(userIDs.front()); } void CertificateDetailsWidget::Private::changeExpiration() { auto cmd = new Kleo::Commands::ChangeExpiryCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangeExpiryCommand::finished, q, [this]() { ui.changeExpirationAction->setEnabled(true); }); ui.changeExpirationAction->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::changePassphrase() { auto cmd = new Kleo::Commands::ChangePassphraseCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangePassphraseCommand::finished, q, [this]() { ui.changePassphraseBtn->setEnabled(true); }); ui.changePassphraseBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::genRevokeCert() { auto cmd = new Kleo::Commands::GenRevokeCommand(key); QObject::connect(cmd, &Kleo::Commands::GenRevokeCommand::finished, q, [this]() { ui.genRevokeBtn->setEnabled(true); }); ui.genRevokeBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::refreshCertificate() { auto cmd = new Kleo::RefreshCertificateCommand{key}; QObject::connect(cmd, &Kleo::RefreshCertificateCommand::finished, q, [this]() { ui.refreshBtn->setEnabled(true); }); ui.refreshBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::certifyUserIDs() { const auto userIDs = selectedUserIDs(ui.userIDTable); auto cmd = userIDs.empty() ? new Kleo::Commands::CertifyCertificateCommand{key} // : new Kleo::Commands::CertifyCertificateCommand{userIDs}; QObject::connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this]() { updateKey(); ui.certifyBtn->setEnabled(true); }); ui.certifyBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::revokeCertifications() { const auto userIDs = selectedUserIDs(ui.userIDTable); auto cmd = userIDs.empty() ? new Kleo::Commands::RevokeCertificationCommand{key} // : new Kleo::Commands::RevokeCertificationCommand{userIDs}; QObject::connect(cmd, &Kleo::Command::finished, q, [this]() { updateKey(); ui.revokeCertificationsBtn->setEnabled(true); }); ui.revokeCertificationsBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::webOfTrustClicked() { QScopedPointer dlg(new WebOfTrustDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::exportClicked() { QScopedPointer dlg(new ExportDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::addUserID() { auto cmd = new Kleo::Commands::AddUserIDCommand(key); QObject::connect(cmd, &Kleo::Commands::AddUserIDCommand::finished, q, [this]() { ui.addUserIDBtn->setEnabled(true); updateKey(); }); ui.addUserIDBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::setPrimaryUserID(const GpgME::UserID &uid) { auto userId = uid; if (userId.isNull()) { const auto userIDs = selectedUserIDs(ui.userIDTable); if (userIDs.size() != 1) { return; } userId = userIDs.front(); } auto cmd = new Kleo::Commands::SetPrimaryUserIDCommand(userId); QObject::connect(cmd, &Kleo::Commands::SetPrimaryUserIDCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); // the Flag As Primary button will be updated by the key update updateKey(); }); ui.userIDTable->setEnabled(false); ui.setPrimaryUserIDBtn->setEnabled(false); cmd->start(); } namespace { void ensureThatKeyDetailsAreLoaded(GpgME::Key &key) { if (key.userID(0).numSignatures() == 0) { key.update(); } } } void CertificateDetailsWidget::Private::keysMayHaveChanged() { auto newKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint()); if (!newKey.isNull()) { ensureThatKeyDetailsAreLoaded(newKey); setUpdatedKey(newKey); } } void CertificateDetailsWidget::Private::showTrustChainDialog() { QScopedPointer dlg(new TrustChainDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::userIDTableContextMenuRequested(const QPoint &p) { const auto userIDs = selectedUserIDs(ui.userIDTable); const auto singleUserID = (userIDs.size() == 1) ? userIDs.front() : GpgME::UserID{}; const bool isPrimaryUserID = !singleUserID.isNull() && (ui.userIDTable->selectedItems().front() == ui.userIDTable->topLevelItem(0)); const bool canSignUserIDs = userHasCertificationKey(); const auto isLocalKey = !isRemoteKey(key); const auto keyCanBeCertified = Kleo::canBeCertified(key); auto menu = new QMenu(q); if (key.hasSecret()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("favorite")), i18nc("@action:inmenu", "Flag as Primary User ID"), q, [this, singleUserID]() { setPrimaryUserID(singleUserID); }); action->setEnabled(!singleUserID.isNull() // && !isPrimaryUserID // && !Kleo::isRevokedOrExpired(singleUserID) // && canBeUsedForSecretKeyOperations(key)); } { const auto actionText = userIDs.empty() ? i18nc("@action:inmenu", "Certify User IDs...") : i18ncp("@action:inmenu", "Certify User ID...", "Certify User IDs...", userIDs.size()); auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-sign")), actionText, q, [this]() { certifyUserIDs(); }); action->setEnabled(isLocalKey && keyCanBeCertified && canSignUserIDs); } if (Kleo::Commands::RevokeCertificationCommand::isSupported()) { const auto actionText = userIDs.empty() ? i18nc("@action:inmenu", "Revoke Certifications...") : i18ncp("@action:inmenu", "Revoke Certification...", "Revoke Certifications...", userIDs.size()); auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), actionText, q, [this]() { revokeCertifications(); }); action->setEnabled(isLocalKey && canSignUserIDs); } #ifdef MAILAKONADI_ENABLED if (key.hasSecret()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18nc("@action:inmenu", "Publish at Mail Provider ..."), q, [this, singleUserID]() { auto cmd = new Kleo::Commands::ExportOpenPGPCertToProviderCommand(singleUserID); ui.userIDTable->setEnabled(false); connect(cmd, &Kleo::Commands::ExportOpenPGPCertToProviderCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); }); cmd->start(); }); action->setEnabled(!singleUserID.isNull()); } #endif // MAILAKONADI_ENABLED { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), i18nc("@action:inmenu", "Revoke User ID"), q, [this, singleUserID]() { revokeUserID(singleUserID); }); action->setEnabled(!singleUserID.isNull() && canCreateCertifications(key) && canRevokeUserID(singleUserID)); } connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); menu->popup(ui.userIDTable->viewport()->mapToGlobal(p)); } void CertificateDetailsWidget::Private::showMoreDetails() { if (key.protocol() == GpgME::CMS) { auto cmd = new Kleo::Commands::DumpCertificateCommand(key); cmd->setParentWidget(q); cmd->setUseDialog(true); cmd->start(); } else { auto dlg = new SubKeysDialog{q}; dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setKey(key); dlg->open(); } } QString CertificateDetailsWidget::Private::tofuTooltipString(const GpgME::UserID &uid) const { const auto tofu = uid.tofuInfo(); if (tofu.isNull()) { return QString(); } QString html = QStringLiteral(""); const auto appendRow = [&html](const QString &lbl, const QString &val) { html += QStringLiteral( "" "" "" "") .arg(lbl, val); }; const auto appendHeader = [this, &html](const QString &hdr) { html += QStringLiteral("") .arg(q->palette().highlight().color().name(), q->palette().highlightedText().color().name(), hdr); }; const auto dateTime = [](long ts) { QLocale l; return ts == 0 ? i18n("never") : l.toString(QDateTime::fromSecsSinceEpoch(ts), QLocale::ShortFormat); }; appendHeader(i18n("Signing")); appendRow(i18n("First message"), dateTime(tofu.signFirst())); appendRow(i18n("Last message"), dateTime(tofu.signLast())); appendRow(i18n("Message count"), QString::number(tofu.signCount())); appendHeader(i18n("Encryption")); appendRow(i18n("First message"), dateTime(tofu.encrFirst())); appendRow(i18n("Last message"), dateTime(tofu.encrLast())); appendRow(i18n("Message count"), QString::number(tofu.encrCount())); html += QStringLiteral("
%1:%2
%3
"); // Make sure the tooltip string is different for each UserID, even if the // data are the same, otherwise the tooltip is not updated and moved when // user moves mouse from one row to another. html += QStringLiteral("").arg(QString::fromUtf8(uid.id())); return html; } QIcon CertificateDetailsWidget::Private::trustLevelIcon(const GpgME::UserID &uid) const { if (updateInProgress) { return QIcon::fromTheme(QStringLiteral("emblem-question")); } switch (uid.validity()) { case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: return QIcon::fromTheme(QStringLiteral("emblem-question")); case GpgME::UserID::Never: return QIcon::fromTheme(QStringLiteral("emblem-error")); case GpgME::UserID::Marginal: return QIcon::fromTheme(QStringLiteral("emblem-warning")); case GpgME::UserID::Full: case GpgME::UserID::Ultimate: return QIcon::fromTheme(QStringLiteral("emblem-success")); } return {}; } QString CertificateDetailsWidget::Private::trustLevelText(const GpgME::UserID &uid) const { return updateInProgress ? i18n("Updating...") : Formatting::validityShort(uid); } namespace { auto isGood(const GpgME::UserID::Signature &signature) { return signature.status() == GpgME::UserID::Signature::NoError // && !signature.isInvalid() // && 0x10 <= signature.certClass() && signature.certClass() <= 0x13; } auto accumulateTrustDomains(const std::vector &signatures) { return std::accumulate(std::begin(signatures), std::end(signatures), std::set(), [](auto domains, const auto &signature) { if (isGood(signature) && signature.isTrustSignature()) { domains.insert(Formatting::trustSignatureDomain(signature)); } return domains; }); } auto accumulateTrustDomains(const std::vector &userIds) { return std::accumulate(std::begin(userIds), std::end(userIds), std::set(), [](auto domains, const auto &userID) { const auto newDomains = accumulateTrustDomains(userID.signatures()); std::copy(std::begin(newDomains), std::end(newDomains), std::inserter(domains, std::end(domains))); return domains; }); } } void CertificateDetailsWidget::Private::setupPGPProperties() { setUpUserIDTable(); const auto trustDomains = accumulateTrustDomains(key.userIDs()); ui.trustedIntroducerField->setVisible(!trustDomains.empty()); ui.trustedIntroducerField->setValue(QStringList(std::begin(trustDomains), std::end(trustDomains)).join(u", ")); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the key from external sources.")); } static QString formatDNToolTip(const Kleo::DN &dn) { QString html = QStringLiteral(""); const auto appendRow = [&html, dn](const QString &lbl, const QString &attr) { const QString val = dn[attr]; if (!val.isEmpty()) { html += QStringLiteral( "" "" "") .arg(lbl, val); } }; appendRow(i18n("Common Name"), QStringLiteral("CN")); appendRow(i18n("Organization"), QStringLiteral("O")); appendRow(i18n("Street"), QStringLiteral("STREET")); appendRow(i18n("City"), QStringLiteral("L")); appendRow(i18n("State"), QStringLiteral("ST")); appendRow(i18n("Country"), QStringLiteral("C")); html += QStringLiteral("
%1:%2
"); return html; } void CertificateDetailsWidget::Private::setupSMIMEProperties() { const auto ownerId = key.userID(0); const Kleo::DN dn(ownerId.id()); for (const auto &[attributeName, field] : ui.smimeAttributeFields) { const QString attributeValue = dn[attributeName]; field->setValue(attributeValue); field->setVisible(!attributeValue.isEmpty()); } ui.smimeTrustLevelField->setIcon(trustLevelIcon(ownerId)); ui.smimeTrustLevelField->setValue(trustLevelText(ownerId)); const Kleo::DN issuerDN(key.issuerName()); const QString issuerCN = issuerDN[QStringLiteral("CN")]; const QString issuer = issuerCN.isEmpty() ? QString::fromUtf8(key.issuerName()) : issuerCN; ui.smimeIssuerField->setValue(issuer); ui.smimeIssuerField->setToolTip(formatDNToolTip(issuerDN)); ui.showIssuerCertificateAction->setEnabled(!key.isRoot()); setUpSMIMEAdressList(); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the CRLs and do a full validation check of the certificate.")); } void CertificateDetailsWidget::Private::showIssuerCertificate() { // there is either one or no parent key const auto parentKeys = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); if (parentKeys.empty()) { KMessageBox::error(q, i18n("The issuer certificate could not be found locally.")); return; } auto cmd = new Kleo::Commands::DetailsCommand(parentKeys.front()); cmd->setParentWidget(q); cmd->start(); } void CertificateDetailsWidget::Private::copyFingerprintToClipboard() { if (auto clipboard = QGuiApplication::clipboard()) { clipboard->setText(QString::fromLatin1(key.primaryFingerprint())); } } CertificateDetailsWidget::CertificateDetailsWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } CertificateDetailsWidget::~CertificateDetailsWidget() = default; void CertificateDetailsWidget::Private::keyListDone(const GpgME::KeyListResult &, const std::vector &keys, const QString &, const GpgME::Error &) { updateInProgress = false; if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Invalid keylist result in update."; return; } // As we listen for keysmayhavechanged we get the update // after updating the keycache. KeyCache::mutableInstance()->insert(keys); } void CertificateDetailsWidget::Private::updateKey() { key.update(); setUpdatedKey(key); } void CertificateDetailsWidget::Private::setUpdatedKey(const GpgME::Key &k) { key = k; setupCommonProperties(); if (key.protocol() == GpgME::OpenPGP) { setupPGPProperties(); } else { setupSMIMEProperties(); } } void CertificateDetailsWidget::setKey(const GpgME::Key &key) { if (key.protocol() == GpgME::CMS) { // For everything but S/MIME this should be quick // and we don't need to show another status. d->updateInProgress = true; } d->setUpdatedKey(key); // Run a keylistjob with full details (TOFU / Validate) QGpgME::KeyListJob *job = key.protocol() == GpgME::OpenPGP ? QGpgME::openpgp()->keyListJob(false, true, true) : QGpgME::smime()->keyListJob(false, true, true); auto ctx = QGpgME::Job::context(job); ctx->addKeyListMode(GpgME::WithTofu); ctx->addKeyListMode(GpgME::SignatureNotations); if (key.hasSecret()) { ctx->addKeyListMode(GpgME::WithSecret); } // Windows QGpgME new style connect problem makes this necessary. connect(job, SIGNAL(result(GpgME::KeyListResult, std::vector, QString, GpgME::Error)), this, SLOT(keyListDone(GpgME::KeyListResult, std::vector, QString, GpgME::Error))); job->start(QStringList() << QLatin1String(key.primaryFingerprint())); } GpgME::Key CertificateDetailsWidget::key() const { return d->key; } #include "moc_certificatedetailswidget.cpp" diff --git a/src/dialogs/createcsrforcardkeydialog.cpp b/src/dialogs/createcsrforcardkeydialog.cpp index fe3702e5b..d5136d797 100644 --- a/src/dialogs/createcsrforcardkeydialog.cpp +++ b/src/dialogs/createcsrforcardkeydialog.cpp @@ -1,117 +1,117 @@ /* -*- mode: c++; c-basic-offset:4 -*- - dialogs/createcsrforcardkeydialog.h + dialogs/createcsrforcardkeydialog.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 "createcsrforcardkeydialog.h" #include "certificatedetailsinputwidget.h" #include #include #include #include #include using namespace Kleo; using namespace Kleo::Dialogs; class CreateCSRForCardKeyDialog::Private { friend class ::Kleo::Dialogs::CreateCSRForCardKeyDialog; CreateCSRForCardKeyDialog *const q; struct { CertificateDetailsInputWidget *detailsWidget = nullptr; QDialogButtonBox *buttonBox = nullptr; } ui; public: Private(CreateCSRForCardKeyDialog *qq) : q(qq) { auto mainLayout = new QVBoxLayout(q); ui.detailsWidget = new CertificateDetailsInputWidget(); connect(ui.detailsWidget, &CertificateDetailsInputWidget::validityChanged, q, [this](bool valid) { onValidityChanged(valid); }); ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept); connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); mainLayout->addWidget(ui.detailsWidget); mainLayout->addWidget(ui.buttonBox); // increase default width by 50 % to get more space for line edits const QSize sizeHint = q->sizeHint(); const QSize defaultSize = QSize(sizeHint.width() * 15 / 10, sizeHint.height()); restoreGeometry(defaultSize); } ~Private() { saveGeometry(); } void onValidityChanged(bool valid) { ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(valid); } private: void saveGeometry() { KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("CreateCSRForCardKeyDialog")); cfgGroup.writeEntry("Size", q->size()); cfgGroup.sync(); } void restoreGeometry(const QSize &defaultSize) { KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("CreateCSRForCardKeyDialog")); const QSize size = cfgGroup.readEntry("Size", defaultSize); if (size.isValid()) { q->resize(size); } } }; CreateCSRForCardKeyDialog::CreateCSRForCardKeyDialog(QWidget *parent) : QDialog(parent) , d(new Private(this)) { } CreateCSRForCardKeyDialog::~CreateCSRForCardKeyDialog() { } void CreateCSRForCardKeyDialog::setName(const QString &name) { d->ui.detailsWidget->setName(name); } void CreateCSRForCardKeyDialog::setEmail(const QString &email) { d->ui.detailsWidget->setEmail(email); } QString CreateCSRForCardKeyDialog::email() const { return d->ui.detailsWidget->email(); } QString CreateCSRForCardKeyDialog::dn() const { return d->ui.detailsWidget->dn(); } #include "moc_createcsrforcardkeydialog.cpp" diff --git a/src/dialogs/expirydialog.cpp b/src/dialogs/expirydialog.cpp index 3a435ce0c..a3c604ba2 100644 --- a/src/dialogs/expirydialog.cpp +++ b/src/dialogs/expirydialog.cpp @@ -1,224 +1,241 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/expirydialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "expirydialog.h" +#include "utils/accessibility.h" #include "utils/expiration.h" #include "utils/gui-helper.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Dialogs; class ExpiryDialog::Private { friend class ::Kleo::Dialogs::ExpiryDialog; ExpiryDialog *const q; public: Private(Mode mode, ExpiryDialog *qq) : q{qq} , ui{mode, qq} { ui.neverRB->setEnabled(unlimitedValidityAllowed()); ui.onRB->setEnabled(!fixedExpirationDate()); connect(ui.onCB, &KDateComboBox::dateChanged, q, [this]() { slotOnDateChanged(); }); } private: void slotOnDateChanged(); private: bool unlimitedValidityAllowed() const; bool fixedExpirationDate() const; void setInitialFocus(); private: bool initialFocusWasSet = false; struct UI { QRadioButton *neverRB; QRadioButton *onRB; KDateComboBox *onCB; QCheckBox *updateSubkeysCheckBox; + QLabel *primaryKeyExpirationDate; + LabelHelper labelHelper; explicit UI(Mode mode, Dialogs::ExpiryDialog *qq) { auto mainLayout = new QVBoxLayout{qq}; auto mainWidget = new QWidget{qq}; auto vboxLayout = new QVBoxLayout{mainWidget}; vboxLayout->setContentsMargins(0, 0, 0, 0); { auto label = new QLabel{qq}; label->setText(mode == Mode::UpdateIndividualSubkey ? i18n("Please select until when the subkey should be valid:") : i18n("Please select until when the certificate should be valid:")); vboxLayout->addWidget(label); } neverRB = new QRadioButton(i18n("Unlimited validity"), mainWidget); neverRB->setChecked(false); vboxLayout->addWidget(neverRB); { auto hboxLayout = new QHBoxLayout; onRB = new QRadioButton{i18n("Valid until:"), mainWidget}; onRB->setChecked(true); hboxLayout->addWidget(onRB); onCB = new KDateComboBox{mainWidget}; setUpExpirationDateComboBox(onCB); hboxLayout->addWidget(onCB); hboxLayout->addStretch(1); vboxLayout->addLayout(hboxLayout); } + primaryKeyExpirationDate = new QLabel(qq); + primaryKeyExpirationDate->setVisible(false); + vboxLayout->addWidget(primaryKeyExpirationDate); + labelHelper.addLabel(primaryKeyExpirationDate); + { updateSubkeysCheckBox = new QCheckBox{i18n("Also update the validity period of the subkeys"), qq}; updateSubkeysCheckBox->setVisible(mode == Mode::UpdateCertificateWithSubkeys); vboxLayout->addWidget(updateSubkeysCheckBox); } vboxLayout->addStretch(1); mainLayout->addWidget(mainWidget); auto buttonBox = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel, qq}; auto okButton = buttonBox->button(QDialogButtonBox::Ok); KGuiItem::assign(okButton, KStandardGuiItem::ok()); okButton->setDefault(true); okButton->setShortcut(static_cast(Qt::CTRL) | static_cast(Qt::Key_Return)); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); qq->connect(buttonBox, &QDialogButtonBox::accepted, qq, &ExpiryDialog::accept); qq->connect(buttonBox, &QDialogButtonBox::rejected, qq, &QDialog::reject); mainLayout->addWidget(buttonBox); connect(onRB, &QRadioButton::toggled, onCB, &QWidget::setEnabled); } } ui; }; void ExpiryDialog::Private::slotOnDateChanged() { ui.onRB->setAccessibleName(i18nc("Valid until DATE", "Valid until %1", Formatting::accessibleDate(ui.onCB->date()))); } bool Kleo::Dialogs::ExpiryDialog::Private::unlimitedValidityAllowed() const { return !Kleo::maximumExpirationDate().isValid(); } bool Kleo::Dialogs::ExpiryDialog::Private::fixedExpirationDate() const { return ui.onCB->minimumDate() == ui.onCB->maximumDate(); } void ExpiryDialog::Private::setInitialFocus() { if (initialFocusWasSet) { return; } // give focus to the checked radio button (void)focusFirstCheckedButton({ui.neverRB, ui.onRB}); initialFocusWasSet = true; } ExpiryDialog::ExpiryDialog(Mode mode, QWidget *p) : QDialog{p} , d{new Private{mode, this}} { setWindowTitle(i18nc("@title:window", "Change Validity Period")); } ExpiryDialog::~ExpiryDialog() = default; void ExpiryDialog::setDateOfExpiry(const QDate &date) { const QDate current = QDate::currentDate(); if (date.isValid()) { d->ui.onRB->setChecked(true); if (date <= current) { d->ui.onCB->setDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); } else { d->ui.onCB->setDate(date); } } else { if (d->unlimitedValidityAllowed()) { d->ui.neverRB->setChecked(true); } else { d->ui.onRB->setChecked(true); } d->ui.onCB->setDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); } } QDate ExpiryDialog::dateOfExpiry() const { return d->ui.onRB->isChecked() ? d->ui.onCB->date() : QDate{}; } void ExpiryDialog::setUpdateExpirationOfAllSubkeys(bool update) { d->ui.updateSubkeysCheckBox->setChecked(update); } bool ExpiryDialog::updateExpirationOfAllSubkeys() const { return d->ui.updateSubkeysCheckBox->isChecked(); } void ExpiryDialog::accept() { const auto date = dateOfExpiry(); if (!Kleo::isValidExpirationDate(date)) { KMessageBox::error(this, i18nc("@info", "Error: %1", Kleo::validityPeriodHint())); return; } QDialog::accept(); } void ExpiryDialog::showEvent(QShowEvent *event) { d->setInitialFocus(); QDialog::showEvent(event); } +void ExpiryDialog::setPrimaryKey(const GpgME::Key &key) +{ + if (!key.subkey(0).neverExpires()) { + d->ui.primaryKeyExpirationDate->setText(i18n("Expiration of primary key: %1", Kleo::Formatting::expirationDateString(key))); + d->ui.onCB->setMaximumDate(Kleo::Formatting::expirationDate(key)); + d->ui.primaryKeyExpirationDate->setVisible(true); + } +} + #include "moc_expirydialog.cpp" diff --git a/src/dialogs/expirydialog.h b/src/dialogs/expirydialog.h index 5ab124b7d..968ddbb1e 100644 --- a/src/dialogs/expirydialog.h +++ b/src/dialogs/expirydialog.h @@ -1,56 +1,62 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/expirydialog.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include class QDate; class QShowEvent; +namespace GpgME +{ +class Key; +} + namespace Kleo { namespace Dialogs { class ExpiryDialog : public QDialog { Q_OBJECT public: enum class Mode { UpdateCertificateWithSubkeys, UpdateCertificateWithoutSubkeys, UpdateIndividualSubkey, }; explicit ExpiryDialog(Mode mode, QWidget *parent = nullptr); ~ExpiryDialog() override; void setDateOfExpiry(const QDate &date); QDate dateOfExpiry() const; void setUpdateExpirationOfAllSubkeys(bool update); bool updateExpirationOfAllSubkeys() const; + void setPrimaryKey(const GpgME::Key &key); void accept() override; protected: void showEvent(QShowEvent *event) override; private: class Private; std::unique_ptr d; }; } } diff --git a/src/dialogs/lookupcertificatesdialog.cpp b/src/dialogs/lookupcertificatesdialog.cpp index 0958e686e..ee201ec59 100644 --- a/src/dialogs/lookupcertificatesdialog.cpp +++ b/src/dialogs/lookupcertificatesdialog.cpp @@ -1,348 +1,417 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/lookupcertificatesdialog.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 "lookupcertificatesdialog.h" -#include "view/keytreeview.h" +#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; -static const int minimalSearchTextLength = 1; - class LookupCertificatesDialog::Private { friend class ::Kleo::Dialogs::LookupCertificatesDialog; LookupCertificatesDialog *const q; public: explicit Private(LookupCertificatesDialog *qq); ~Private(); private: void slotSelectionChanged() { enableDisableWidgets(); } void slotSearchTextChanged() { enableDisableWidgets(); } void slotSearchClicked() { - Q_EMIT q->searchTextChanged(ui.findED->text()); + Q_EMIT q->searchTextChanged(searchText()); } void slotDetailsClicked() { Q_ASSERT(q->selectedCertificates().size() == 1); Q_EMIT q->detailsRequested(q->selectedCertificates().front()); } void slotSaveAsClicked() { Q_EMIT q->saveAsRequested(q->selectedCertificates()); } void readConfig(); void writeConfig(); void enableDisableWidgets(); QString searchText() const { return ui.findED->text().trimmed(); } std::vector selectedCertificates() const { const QAbstractItemView *const view = ui.resultTV->view(); if (!view) { return std::vector(); } const auto *const model = dynamic_cast(view->model()); Q_ASSERT(model); const QItemSelectionModel *const sm = view->selectionModel(); Q_ASSERT(sm); return model->keys(sm->selectedRows()); } int numSelectedCertificates() const { return ui.resultTV->selectedKeys().size(); } + QValidator *queryValidator(); + void updateQueryMode(); + private: + QueryMode queryMode = AnyQuery; bool passive; + QValidator *anyQueryValidator = nullptr; + QValidator *emailQueryValidator = nullptr; struct Ui { + QLabel *guidanceLabel; QLabel *findLB; QLineEdit *findED; QPushButton *findPB; Kleo::KeyTreeView *resultTV; QPushButton *selectAllPB; QPushButton *deselectAllPB; QPushButton *detailsPB; QPushButton *saveAsPB; KMessageWidget *messageWidget; QDialogButtonBox *buttonBox; void setupUi(QDialog *dialog) { auto verticalLayout = new QVBoxLayout{dialog}; auto gridLayout = new QGridLayout{}; int row = 0; + guidanceLabel = new QLabel{dialog}; + gridLayout->addWidget(guidanceLabel, row, 0, 1, 3); + + row++; findLB = new QLabel{i18n("Find:"), dialog}; gridLayout->addWidget(findLB, row, 0, 1, 1); findED = new QLineEdit{dialog}; findLB->setBuddy(findED); gridLayout->addWidget(findED, row, 1, 1, 1); findPB = new QPushButton{i18n("Search"), dialog}; findPB->setAutoDefault(false); gridLayout->addWidget(findPB, row, 2, 1, 1); row++; gridLayout->addWidget(new KSeparator{Qt::Horizontal, dialog}, row, 0, 1, 3); row++; resultTV = new Kleo::KeyTreeView(dialog); resultTV->setEnabled(true); resultTV->setMinimumSize(QSize(400, 0)); gridLayout->addWidget(resultTV, row, 0, 1, 2); auto buttonLayout = new QVBoxLayout{}; selectAllPB = new QPushButton{i18n("Select All"), dialog}; selectAllPB->setEnabled(false); selectAllPB->setAutoDefault(false); buttonLayout->addWidget(selectAllPB); deselectAllPB = new QPushButton{i18n("Deselect All"), dialog}; deselectAllPB->setEnabled(false); deselectAllPB->setAutoDefault(false); buttonLayout->addWidget(deselectAllPB); buttonLayout->addStretch(); detailsPB = new QPushButton{i18n("Details..."), dialog}; detailsPB->setEnabled(false); detailsPB->setAutoDefault(false); buttonLayout->addWidget(detailsPB); saveAsPB = new QPushButton{i18n("Save As..."), dialog}; saveAsPB->setEnabled(false); saveAsPB->setAutoDefault(false); buttonLayout->addWidget(saveAsPB); gridLayout->addLayout(buttonLayout, row, 2, 1, 1); row++; messageWidget = new KMessageWidget{dialog}; messageWidget->setMessageType(KMessageWidget::Information); messageWidget->setVisible(false); gridLayout->addWidget(messageWidget, row, 0, 1, 3); verticalLayout->addLayout(gridLayout); buttonBox = new QDialogButtonBox{dialog}; buttonBox->setStandardButtons(QDialogButtonBox::Close | QDialogButtonBox::Save); verticalLayout->addWidget(buttonBox); QObject::connect(findED, SIGNAL(returnPressed()), findPB, SLOT(animateClick())); QObject::connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); QObject::connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); QObject::connect(findPB, SIGNAL(clicked()), dialog, SLOT(slotSearchClicked())); QObject::connect(detailsPB, SIGNAL(clicked()), dialog, SLOT(slotDetailsClicked())); QObject::connect(saveAsPB, SIGNAL(clicked()), dialog, SLOT(slotSaveAsClicked())); QObject::connect(findED, SIGNAL(textChanged(QString)), dialog, SLOT(slotSearchTextChanged())); QMetaObject::connectSlotsByName(dialog); } explicit Ui(LookupCertificatesDialog *q) { q->setWindowTitle(i18nc("@title:window", "Lookup on Server")); setupUi(q); saveAsPB->hide(); // ### not yet implemented in LookupCertificatesCommand findED->setClearButtonEnabled(true); resultTV->setFlatModel(AbstractKeyListModel::createFlatKeyListModel(q)); resultTV->setHierarchicalView(false); importPB()->setText(i18n("Import")); importPB()->setEnabled(false); connect(resultTV->view(), SIGNAL(doubleClicked(QModelIndex)), importPB(), SLOT(animateClick())); findED->setFocus(); connect(selectAllPB, &QPushButton::clicked, resultTV->view(), &QTreeView::selectAll); connect(deselectAllPB, &QPushButton::clicked, resultTV->view(), &QTreeView::clearSelection); } QPushButton *importPB() const { return buttonBox->button(QDialogButtonBox::Save); } QPushButton *closePB() const { return buttonBox->button(QDialogButtonBox::Close); } } ui; }; LookupCertificatesDialog::Private::Private(LookupCertificatesDialog *qq) : q(qq) , passive(false) , ui(q) { connect(ui.resultTV->view()->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), q, SLOT(slotSelectionChanged())); + updateQueryMode(); } LookupCertificatesDialog::Private::~Private() { } void LookupCertificatesDialog::Private::readConfig() { KConfigGroup configGroup(KSharedConfig::openStateConfig(), QStringLiteral("LookupCertificatesDialog")); KConfigGroup resultKeysConfig = configGroup.group(QStringLiteral("ResultKeysView")); ui.resultTV->restoreLayout(resultKeysConfig); const QSize size = configGroup.readEntry("Size", QSize(600, 400)); if (size.isValid()) { q->resize(size); } } void LookupCertificatesDialog::Private::writeConfig() { KConfigGroup configGroup(KSharedConfig::openStateConfig(), QStringLiteral("LookupCertificatesDialog")); configGroup.writeEntry("Size", q->size()); KConfigGroup resultKeysConfig = configGroup.group(QStringLiteral("ResultKeysView")); ui.resultTV->saveLayout(resultKeysConfig); configGroup.sync(); } +static QString guidanceText(LookupCertificatesDialog::QueryMode mode) +{ + switch (mode) { + default: + qCWarning(KLEOPATRA_LOG) << __func__ << "Unknown query mode:" << mode; + [[fallthrough]]; + case LookupCertificatesDialog::AnyQuery: + return xi18nc("@info", "Enter a search term to search for matching certificates."); + case LookupCertificatesDialog::EmailQuery: + return xi18nc("@info", "Enter an email address to search for matching certificates."); + }; +} + +QValidator *LookupCertificatesDialog::Private::queryValidator() +{ + switch (queryMode) { + default: + qCWarning(KLEOPATRA_LOG) << __func__ << "Unknown query mode:" << queryMode; + [[fallthrough]]; + case AnyQuery: { + if (!anyQueryValidator) { + // allow any query with at least one non-whitespace character + anyQueryValidator = new QRegularExpressionValidator{QRegularExpression{QStringLiteral(".*\\S+.*")}, q}; + } + return anyQueryValidator; + } + case EmailQuery: { + if (!emailQueryValidator) { + // allow anything that looks remotely like an email address, i.e. + // anything with an '@' surrounded by non-whitespace characters + const QRegularExpression simpleEmailRegex{QStringLiteral(".*\\S+@\\S+.*")}; + emailQueryValidator = new QRegularExpressionValidator{simpleEmailRegex, q}; + } + return emailQueryValidator; + } + } +} + +void LookupCertificatesDialog::Private::updateQueryMode() +{ + ui.guidanceLabel->setText(guidanceText(queryMode)); + ui.findED->setValidator(queryValidator()); +} + LookupCertificatesDialog::LookupCertificatesDialog(QWidget *p, Qt::WindowFlags f) : QDialog(p, f) , d(new Private(this)) { d->ui.findPB->setEnabled(false); d->readConfig(); } LookupCertificatesDialog::~LookupCertificatesDialog() { d->writeConfig(); } +void LookupCertificatesDialog::setQueryMode(QueryMode mode) +{ + d->queryMode = mode; + d->updateQueryMode(); +} + +LookupCertificatesDialog::QueryMode LookupCertificatesDialog::queryMode() const +{ + return d->queryMode; +} + void LookupCertificatesDialog::setCertificates(const std::vector &certs) { d->ui.resultTV->view()->setFocus(); d->ui.resultTV->setKeys(certs); } std::vector LookupCertificatesDialog::selectedCertificates() const { return d->selectedCertificates(); } void LookupCertificatesDialog::setPassive(bool on) { if (d->passive == on) { return; } d->passive = on; d->enableDisableWidgets(); } bool LookupCertificatesDialog::isPassive() const { return d->passive; } void LookupCertificatesDialog::setSearchText(const QString &text) { d->ui.findED->setText(text); } QString LookupCertificatesDialog::searchText() const { return d->ui.findED->text(); } void LookupCertificatesDialog::showInformation(const QString &message) { d->ui.messageWidget->setText(message); if (message.isEmpty()) { d->ui.messageWidget->animatedHide(); } else { d->ui.messageWidget->animatedShow(); } } void LookupCertificatesDialog::accept() { Q_ASSERT(!selectedCertificates().empty()); Q_EMIT importRequested(selectedCertificates()); QDialog::accept(); } void LookupCertificatesDialog::Private::enableDisableWidgets() { // enable/disable everything except 'close', based on passive: const QList list = q->children(); for (QObject *const o : list) { if (QWidget *const w = qobject_cast(o)) { w->setDisabled(passive && w != ui.closePB() && w != ui.buttonBox); } } if (passive) { return; } - ui.findPB->setEnabled(searchText().size() >= minimalSearchTextLength); + ui.findPB->setEnabled(ui.findED->hasAcceptableInput()); const int n = q->selectedCertificates().size(); ui.detailsPB->setEnabled(n == 1); ui.saveAsPB->setEnabled(n == 1); ui.importPB()->setEnabled(n != 0); ui.importPB()->setDefault(false); // otherwise Import becomes default button if enabled and return triggers both a search and accept() } #include "moc_lookupcertificatesdialog.cpp" diff --git a/src/dialogs/lookupcertificatesdialog.h b/src/dialogs/lookupcertificatesdialog.h index 7295e3903..82bacf310 100644 --- a/src/dialogs/lookupcertificatesdialog.h +++ b/src/dialogs/lookupcertificatesdialog.h @@ -1,64 +1,72 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/lookupcertificatesdialog.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include #include namespace GpgME { class Key; } namespace Kleo { namespace Dialogs { class LookupCertificatesDialog : public QDialog { Q_OBJECT public: + enum QueryMode { + AnyQuery, //< any query is allowed + EmailQuery, //< only email queries are allowed + }; + explicit LookupCertificatesDialog(QWidget *parent = nullptr, Qt::WindowFlags f = {}); ~LookupCertificatesDialog() override; + void setQueryMode(QueryMode mode); + QueryMode queryMode() const; + void setCertificates(const std::vector &certs); std::vector selectedCertificates() const; void setPassive(bool passive); bool isPassive() const; void setSearchText(const QString &text); QString searchText() const; void showInformation(const QString &message); Q_SIGNALS: void searchTextChanged(const QString &text); void saveAsRequested(const std::vector &certs); void importRequested(const std::vector &certs); void detailsRequested(const GpgME::Key &certs); public Q_SLOTS: void accept() override; private: class Private; kdtools::pimpl_ptr d; Q_PRIVATE_SLOT(d, void slotSearchTextChanged()) Q_PRIVATE_SLOT(d, void slotSearchClicked()) Q_PRIVATE_SLOT(d, void slotSelectionChanged()) Q_PRIVATE_SLOT(d, void slotDetailsClicked()) Q_PRIVATE_SLOT(d, void slotSaveAsClicked()) }; } } diff --git a/src/dialogs/revokekeydialog.cpp b/src/dialogs/revokekeydialog.cpp index b729ea0f6..a7c2637d4 100644 --- a/src/dialogs/revokekeydialog.cpp +++ b/src/dialogs/revokekeydialog.cpp @@ -1,288 +1,288 @@ /* -*- mode: c++; c-basic-offset:4 -*- - dialogs/revokekeydialog.h + dialogs/revokekeydialog.cpp 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 */ #include "revokekeydialog.h" #include "utils/accessibility.h" #include "view/errorlabel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; namespace { class TextEdit : public QTextEdit { Q_OBJECT public: using QTextEdit::QTextEdit; Q_SIGNALS: void editingFinished(); protected: void focusOutEvent(QFocusEvent *event) override { Qt::FocusReason reason = event->reason(); if (reason != Qt::PopupFocusReason || !(QApplication::activePopupWidget() && QApplication::activePopupWidget()->parentWidget() == this)) { Q_EMIT editingFinished(); } QTextEdit::focusOutEvent(event); } }; } class RevokeKeyDialog::Private { friend class ::Kleo::RevokeKeyDialog; RevokeKeyDialog *const q; struct { QLabel *infoLabel = nullptr; QLabel *descriptionLabel = nullptr; TextEdit *description = nullptr; ErrorLabel *descriptionError = nullptr; QDialogButtonBox *buttonBox = nullptr; } ui; Key key; QButtonGroup reasonGroup; bool descriptionEditingInProgress = false; QString descriptionAccessibleName; public: Private(RevokeKeyDialog *qq) : q(qq) { q->setWindowTitle(i18nc("title:window", "Revoke Key")); auto mainLayout = new QVBoxLayout{q}; ui.infoLabel = new QLabel{q}; mainLayout->addWidget(ui.infoLabel); auto groupBox = new QGroupBox{i18nc("@title:group", "Reason for revocation"), q}; reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "No reason specified"), q}, static_cast(RevocationReason::Unspecified)); reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "Key has been compromised"), q}, static_cast(RevocationReason::Compromised)); reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "Key is superseded"), q}, static_cast(RevocationReason::Superseded)); reasonGroup.addButton(new QRadioButton{i18nc("@option:radio", "Key is no longer used"), q}, static_cast(RevocationReason::NoLongerUsed)); reasonGroup.button(static_cast(RevocationReason::Unspecified))->setChecked(true); { auto boxLayout = new QVBoxLayout{groupBox}; for (auto radio : reasonGroup.buttons()) { boxLayout->addWidget(radio); } } mainLayout->addWidget(groupBox); { ui.descriptionLabel = new QLabel{i18nc("@label:textbox", "Description (optional):"), q}; ui.description = new TextEdit{q}; ui.description->setAcceptRichText(false); // do not accept Tab as input; this is better for accessibility and // tabulators are not really that useful in the description ui.description->setTabChangesFocus(true); ui.descriptionLabel->setBuddy(ui.description); ui.descriptionError = new ErrorLabel{q}; ui.descriptionError->setVisible(false); mainLayout->addWidget(ui.descriptionLabel); mainLayout->addWidget(ui.description); mainLayout->addWidget(ui.descriptionError); } connect(ui.description, &TextEdit::editingFinished, q, [this]() { onDescriptionEditingFinished(); }); connect(ui.description, &TextEdit::textChanged, q, [this]() { onDescriptionTextChanged(); }); ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); auto okButton = ui.buttonBox->button(QDialogButtonBox::Ok); okButton->setText(i18nc("@action:button", "Revoke Key")); okButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete-remove"))); mainLayout->addWidget(ui.buttonBox); connect(ui.buttonBox, &QDialogButtonBox::accepted, q, [this]() { checkAccept(); }); connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); restoreGeometry(); } ~Private() { saveGeometry(); } private: void saveGeometry() { KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("RevokeKeyDialog")); cfgGroup.writeEntry("Size", q->size()); cfgGroup.sync(); } void restoreGeometry(const QSize &defaultSize = {}) { KConfigGroup cfgGroup(KSharedConfig::openStateConfig(), QStringLiteral("RevokeKeyDialog")); const QSize size = cfgGroup.readEntry("Size", defaultSize); if (size.isValid()) { q->resize(size); } } void checkAccept() { if (!descriptionHasAcceptableInput()) { KMessageBox::error(q, descriptionErrorMessage()); } else { q->accept(); } } bool descriptionHasAcceptableInput() const { return !q->description().contains(QLatin1String{"\n\n"}); } QString descriptionErrorMessage() const { QString message; if (!descriptionHasAcceptableInput()) { message = i18n("Error: The description must not contain empty lines."); } return message; } void updateDescriptionError() { const auto currentErrorMessage = ui.descriptionError->text(); const auto newErrorMessage = descriptionErrorMessage(); if (newErrorMessage == currentErrorMessage) { return; } if (currentErrorMessage.isEmpty() && descriptionEditingInProgress) { // delay showing the error message until editing is finished, so that we // do not annoy the user with an error message while they are still // entering the recipient; // on the other hand, we clear the error message immediately if it does // not apply anymore and we update the error message immediately if it // changed return; } ui.descriptionError->setVisible(!newErrorMessage.isEmpty()); ui.descriptionError->setText(newErrorMessage); updateAccessibleNameAndDescription(); } void updateAccessibleNameAndDescription() { // fall back to default accessible name if accessible name wasn't set explicitly if (descriptionAccessibleName.isEmpty()) { descriptionAccessibleName = getAccessibleName(ui.description); } const bool errorShown = ui.descriptionError->isVisible(); // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute); // emulate this by setting the error message as accessible description of the input field const auto description = errorShown ? ui.descriptionError->text() : QString{}; if (ui.description->accessibleDescription() != description) { ui.description->setAccessibleDescription(description); } // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute); // screen readers say something like "invalid entry" if this state is set; // emulate this by adding "invalid entry" to the accessible name of the input field // and its label const auto name = errorShown ? descriptionAccessibleName + QLatin1String{", "} + invalidEntryText() // : descriptionAccessibleName; if (ui.descriptionLabel->accessibleName() != name) { ui.descriptionLabel->setAccessibleName(name); } if (ui.description->accessibleName() != name) { ui.description->setAccessibleName(name); } } void onDescriptionTextChanged() { descriptionEditingInProgress = true; updateDescriptionError(); } void onDescriptionEditingFinished() { descriptionEditingInProgress = false; updateDescriptionError(); } }; RevokeKeyDialog::RevokeKeyDialog(QWidget *parent, Qt::WindowFlags f) : QDialog{parent, f} , d{new Private{this}} { } RevokeKeyDialog::~RevokeKeyDialog() = default; void RevokeKeyDialog::setKey(const GpgME::Key &key) { d->key = key; d->ui.infoLabel->setText(xi18n("You are about to revoke the following key:%1").arg(Formatting::summaryLine(key))); } GpgME::RevocationReason RevokeKeyDialog::reason() const { return static_cast(d->reasonGroup.checkedId()); } QString RevokeKeyDialog::description() const { static const QRegularExpression whitespaceAtEndOfLine{QStringLiteral(R"([ \t\r]+\n)")}; static const QRegularExpression trailingWhitespace{QStringLiteral(R"(\s*$)")}; return d->ui.description->toPlainText().remove(whitespaceAtEndOfLine).remove(trailingWhitespace); } #include "revokekeydialog.moc" #include "moc_revokekeydialog.cpp" diff --git a/src/dialogs/subkeyswidget.cpp b/src/dialogs/subkeyswidget.cpp index 64962355d..de7a93018 100644 --- a/src/dialogs/subkeyswidget.cpp +++ b/src/dialogs/subkeyswidget.cpp @@ -1,319 +1,413 @@ /* dialogs/subkeyswidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "subkeyswidget.h" #include "commands/changeexpirycommand.h" #include "commands/exportsecretsubkeycommand.h" #include "commands/importpaperkeycommand.h" #include "commands/keytocardcommand.h" #include "exportdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(GpgME::Subkey) using namespace Kleo; using namespace Kleo::Commands; class SubKeysWidget::Private { SubKeysWidget *const q; public: Private(SubKeysWidget *qq) : q{qq} , ui{qq} { ui.subkeysTree->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui.subkeysTree, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) { tableContextMenuRequested(p); }); connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { keysMayHaveChanged(); }); } + void changeValidity(const GpgME::Subkey &subkey); + void exportSSH(const GpgME::Subkey &subkey); + void keyToCard(const GpgME::Subkey &subkey); + void exportSecret(const GpgME::Subkey &subkey); + void importPaperKey(); + private: void tableContextMenuRequested(const QPoint &p); void keysMayHaveChanged(); public: GpgME::Key key; public: struct UI { QVBoxLayout *mainLayout; NavigatableTreeWidget *subkeysTree; + QPushButton *changeValidityBtn = nullptr; + QPushButton *exportOpenSSHBtn = nullptr; + QPushButton *restoreBtn = nullptr; + QPushButton *transferToSmartcardBtn = nullptr; + QPushButton *exportSecretBtn = nullptr; + UI(QWidget *widget) { mainLayout = new QVBoxLayout{widget}; mainLayout->setContentsMargins(0, 0, 0, 0); auto subkeysTreeLabel = new QLabel{i18nc("@label", "Subkeys:"), widget}; mainLayout->addWidget(subkeysTreeLabel); subkeysTree = new NavigatableTreeWidget{widget}; subkeysTreeLabel->setBuddy(subkeysTree); subkeysTree->setAccessibleName(i18nc("@label", "Subkeys")); subkeysTree->setRootIsDecorated(false); subkeysTree->setHeaderLabels({ i18nc("@title:column", "ID"), i18nc("@title:column", "Type"), i18nc("@title:column", "Valid From"), i18nc("@title:column", "Valid Until"), i18nc("@title:column", "Status"), i18nc("@title:column", "Strength"), i18nc("@title:column", "Usage"), i18nc("@title:column", "Primary"), i18nc("@title:column", "Storage"), }); mainLayout->addWidget(subkeysTree); + + { + auto buttonRow = new QHBoxLayout; + + changeValidityBtn = new QPushButton(i18nc("@action:button", "Change validity"), widget); + buttonRow->addWidget(changeValidityBtn); + + exportOpenSSHBtn = new QPushButton{i18nc("@action:button", "Export OpenSSH key"), widget}; + buttonRow->addWidget(exportOpenSSHBtn); + + restoreBtn = new QPushButton(i18nc("@action:button", "Restore printed backup"), widget); + buttonRow->addWidget(restoreBtn); + + transferToSmartcardBtn = new QPushButton(i18nc("@action:button", "Transfer to smartcard"), widget); + buttonRow->addWidget(transferToSmartcardBtn); + + exportSecretBtn = new QPushButton(i18nc("@action:button", "Export secret subkey"), widget); + buttonRow->addWidget(exportSecretBtn); + + buttonRow->addStretch(1); + + mainLayout->addLayout(buttonRow); + } } } ui; }; +void SubKeysWidget::Private::changeValidity(const GpgME::Subkey &subkey) +{ + auto cmd = new ChangeExpiryCommand(subkey.parent()); + cmd->setSubkey(subkey); + ui.subkeysTree->setEnabled(false); + connect(cmd, &ChangeExpiryCommand::finished, q, [this]() { + ui.subkeysTree->setEnabled(true); + key.update(); + q->setKey(key); + }); + cmd->setParentWidget(q); + cmd->start(); +} + +void SubKeysWidget::Private::exportSSH(const GpgME::Subkey &subkey) +{ + QScopedPointer dlg(new ExportDialog(q)); + dlg->setKey(subkey, static_cast(GpgME::Context::ExportSSH)); + dlg->exec(); +} + +void SubKeysWidget::Private::importPaperKey() +{ + auto cmd = new ImportPaperKeyCommand(key); + ui.subkeysTree->setEnabled(false); + connect(cmd, &ImportPaperKeyCommand::finished, q, [this]() { + ui.subkeysTree->setEnabled(true); + }); + cmd->setParentWidget(q); + cmd->start(); +} + +void SubKeysWidget::Private::keyToCard(const GpgME::Subkey &subkey) +{ + auto cmd = new KeyToCardCommand(subkey); + ui.subkeysTree->setEnabled(false); + connect(cmd, &KeyToCardCommand::finished, q, [this]() { + ui.subkeysTree->setEnabled(true); + }); + cmd->setParentWidget(q); + cmd->start(); +} + +void SubKeysWidget::Private::exportSecret(const GpgME::Subkey &subkey) +{ + auto cmd = new ExportSecretSubkeyCommand{{subkey}}; + ui.subkeysTree->setEnabled(false); + connect(cmd, &ExportSecretSubkeyCommand::finished, q, [this]() { + ui.subkeysTree->setEnabled(true); + }); + cmd->setParentWidget(q); + cmd->start(); +} + void SubKeysWidget::Private::tableContextMenuRequested(const QPoint &p) { auto item = ui.subkeysTree->itemAt(p); if (!item) { return; } const auto subkey = item->data(0, Qt::UserRole).value(); const bool isOwnKey = subkey.parent().hasSecret(); const bool secretSubkeyStoredInKeyRing = subkey.isSecret() && !subkey.isCardKey(); auto menu = new QMenu(q); connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); if (isOwnKey) { - auto action = menu->addAction(i18n("Change End of Validity Period..."), q, [this, subkey]() { - auto cmd = new ChangeExpiryCommand(subkey.parent()); - cmd->setSubkey(subkey); - ui.subkeysTree->setEnabled(false); - connect(cmd, &ChangeExpiryCommand::finished, q, [this]() { - ui.subkeysTree->setEnabled(true); - key.update(); - q->setKey(key); - }); - cmd->setParentWidget(q); - cmd->start(); + auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("change-date-symbolic")), i18n("Change validity"), q, [this, subkey]() { + changeValidity(subkey); }); action->setEnabled(canBeUsedForSecretKeyOperations(subkey.parent())); } if (subkey.canAuthenticate()) { menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export OpenSSH key"), q, [this, subkey]() { - QScopedPointer dlg(new ExportDialog(q)); - dlg->setKey(subkey, static_cast(GpgME::Context::ExportSSH)); - dlg->exec(); + exportSSH(subkey); }); } auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-import")), i18n("Restore printed backup"), q, [this, subkey]() { - auto cmd = new ImportPaperKeyCommand(subkey.parent()); - ui.subkeysTree->setEnabled(false); - connect(cmd, &ImportPaperKeyCommand::finished, q, [this]() { - ui.subkeysTree->setEnabled(true); - }); - cmd->setParentWidget(q); - cmd->start(); + importPaperKey(); }); + action->setEnabled(!secretSubkeyStoredInKeyRing); if (isOwnKey) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("send-to-symbolic")), i18n("Transfer to smartcard"), q, [this, subkey]() { - auto cmd = new KeyToCardCommand(subkey); - ui.subkeysTree->setEnabled(false); - connect(cmd, &KeyToCardCommand::finished, q, [this]() { - ui.subkeysTree->setEnabled(true); - }); - cmd->setParentWidget(q); - cmd->start(); + keyToCard(subkey); }); action->setEnabled(secretSubkeyStoredInKeyRing && !KeyToCardCommand::getSuitableCards(subkey).empty()); } const bool isPrimarySubkey = subkey.keyID() == key.keyID(); if (isOwnKey && !isPrimarySubkey) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export secret subkey"), q, [this, subkey]() { - auto cmd = new ExportSecretSubkeyCommand{{subkey}}; - ui.subkeysTree->setEnabled(false); - connect(cmd, &ExportSecretSubkeyCommand::finished, q, [this]() { - ui.subkeysTree->setEnabled(true); - }); - cmd->setParentWidget(q); - cmd->start(); + exportSecret(subkey); }); action->setEnabled(secretSubkeyStoredInKeyRing); } menu->popup(ui.subkeysTree->viewport()->mapToGlobal(p)); } void SubKeysWidget::Private::keysMayHaveChanged() { qCDebug(KLEOPATRA_LOG) << q << __func__; const auto updatedKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint()); if (!updatedKey.isNull()) { q->setKey(updatedKey); } } SubKeysWidget::SubKeysWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { + connect(d->ui.subkeysTree, &NavigatableTreeWidget::currentItemChanged, this, [this] { + const auto currentIndex = d->ui.subkeysTree->currentIndex().row(); + const auto &subkey = d->key.subkey(currentIndex); + const bool secretSubkeyStoredInKeyRing = subkey.isSecret() && !subkey.isCardKey(); + d->ui.exportOpenSSHBtn->setEnabled(subkey.canAuthenticate()); + d->ui.changeValidityBtn->setEnabled(d->key.hasSecret() && canBeUsedForSecretKeyOperations(subkey.parent())); + d->ui.exportSecretBtn->setEnabled(d->key.hasSecret() && subkey.fingerprint() != d->key.primaryFingerprint() && secretSubkeyStoredInKeyRing); + d->ui.restoreBtn->setEnabled(!secretSubkeyStoredInKeyRing); + d->ui.transferToSmartcardBtn->setEnabled(secretSubkeyStoredInKeyRing && !KeyToCardCommand::getSuitableCards(subkey).empty()); + }); + connect(d->ui.changeValidityBtn, &QPushButton::clicked, this, [this] { + d->changeValidity(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); + }); + connect(d->ui.exportOpenSSHBtn, &QPushButton::clicked, this, [this] { + d->exportSSH(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); + }); + connect(d->ui.restoreBtn, &QPushButton::clicked, this, [this] { + d->importPaperKey(); + }); + connect(d->ui.transferToSmartcardBtn, &QPushButton::clicked, this, [this] { + d->keyToCard(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); + }); + connect(d->ui.exportSecretBtn, &QPushButton::clicked, this, [this] { + d->exportSecret(d->key.subkey(d->ui.subkeysTree->currentIndex().row())); + }); } SubKeysWidget::~SubKeysWidget() = default; void SubKeysWidget::setKey(const GpgME::Key &key) { if (key.protocol() != GpgME::OpenPGP) { return; } d->key = key; const auto currentItem = d->ui.subkeysTree->currentItem(); const QByteArray selectedKeyFingerprint = currentItem ? QByteArray(currentItem->data(0, Qt::UserRole).value().fingerprint()) : QByteArray(); d->ui.subkeysTree->clear(); const auto subkeys = key.subkeys(); for (const auto &subkey : subkeys) { auto item = new QTreeWidgetItem; item->setData(0, Qt::DisplayRole, Formatting::prettyID(subkey.keyID())); item->setData(0, Qt::AccessibleTextRole, Formatting::accessibleHexID(subkey.keyID())); item->setData(0, Qt::UserRole, QVariant::fromValue(subkey)); item->setData(1, Qt::DisplayRole, Kleo::Formatting::type(subkey)); item->setData(2, Qt::DisplayRole, Kleo::Formatting::creationDateString(subkey)); item->setData(2, Qt::AccessibleTextRole, Formatting::accessibleCreationDate(subkey)); - item->setData(3, Qt::DisplayRole, Kleo::Formatting::expirationDateString(subkey)); - item->setData(3, Qt::AccessibleTextRole, Formatting::accessibleExpirationDate(subkey)); + item->setData(3, + Qt::DisplayRole, + subkey.neverExpires() ? Kleo::Formatting::expirationDateString(subkey.parent()) : Kleo::Formatting::expirationDateString(subkey)); + item->setData(3, + Qt::AccessibleTextRole, + subkey.neverExpires() ? Kleo::Formatting::accessibleExpirationDate(subkey.parent()) : Kleo::Formatting::accessibleExpirationDate(subkey)); item->setData(4, Qt::DisplayRole, Kleo::Formatting::validityShort(subkey)); switch (subkey.publicKeyAlgorithm()) { case GpgME::Subkey::AlgoECDSA: case GpgME::Subkey::AlgoEDDSA: case GpgME::Subkey::AlgoECDH: item->setData(5, Qt::DisplayRole, QString::fromStdString(subkey.algoName())); break; default: item->setData(5, Qt::DisplayRole, QString::number(subkey.length())); } item->setData(6, Qt::DisplayRole, Kleo::Formatting::usageString(subkey)); const auto isPrimary = subkey.keyID() == key.keyID(); item->setData(7, Qt::DisplayRole, isPrimary ? QStringLiteral("✓") : QString()); item->setData(7, Qt::AccessibleTextRole, isPrimary ? i18nc("yes, is primary key", "yes") : i18nc("no, is not primary key", "no")); if (subkey.isCardKey()) { if (const char *serialNo = subkey.cardSerialNumber()) { item->setData(8, Qt::DisplayRole, i18nc("smart card ", "smart card %1", QString::fromUtf8(serialNo))); } else { item->setData(8, Qt::DisplayRole, i18n("smart card")); } } else if (isPrimary && key.hasSecret() && !subkey.isSecret()) { item->setData(8, Qt::DisplayRole, i18nc("key is 'offline key', i.e. secret key is not stored on this computer", "offline")); } else if (subkey.isSecret()) { item->setData(8, Qt::DisplayRole, i18n("on this computer")); } else { item->setData(8, Qt::DisplayRole, i18nc("unknown storage location", "unknown")); } d->ui.subkeysTree->addTopLevelItem(item); if (subkey.fingerprint() == selectedKeyFingerprint) { d->ui.subkeysTree->setCurrentItem(item); } } if (!key.hasSecret()) { // hide information about storage location for keys of other people d->ui.subkeysTree->hideColumn(8); } d->ui.subkeysTree->header()->resizeSections(QHeaderView::ResizeToContents); + + d->ui.changeValidityBtn->setVisible(key.hasSecret()); + d->ui.exportSecretBtn->setVisible(key.hasSecret()); + d->ui.transferToSmartcardBtn->setVisible(key.hasSecret()); } GpgME::Key SubKeysWidget::key() const { return d->key; } SubKeysDialog::SubKeysDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Subkeys Details")); auto l = new QVBoxLayout(this); l->addWidget(new SubKeysWidget(this)); auto bbox = new QDialogButtonBox(this); auto btn = bbox->addButton(QDialogButtonBox::Close); connect(btn, &QPushButton::clicked, this, &QDialog::accept); l->addWidget(bbox); readConfig(); } SubKeysDialog::~SubKeysDialog() { writeConfig(); } void SubKeysDialog::readConfig() { KConfigGroup dialog(KSharedConfig::openStateConfig(), QStringLiteral("SubKeysDialog")); const QSize size = dialog.readEntry("Size", QSize(820, 280)); if (size.isValid()) { resize(size); } } void SubKeysDialog::writeConfig() { KConfigGroup dialog(KSharedConfig::openStateConfig(), QStringLiteral("SubKeysDialog")); dialog.writeEntry("Size", size()); dialog.sync(); } void SubKeysDialog::setKey(const GpgME::Key &key) { auto w = findChild(); Q_ASSERT(w); w->setKey(key); } GpgME::Key SubKeysDialog::key() const { auto w = findChild(); Q_ASSERT(w); return w->key(); } #include "moc_subkeyswidget.cpp" diff --git a/src/kcfg/settings.kcfg b/src/kcfg/settings.kcfg index ca4712fe3..064889855 100644 --- a/src/kcfg/settings.kcfg +++ b/src/kcfg/settings.kcfg @@ -1,205 +1,215 @@ This text will be used as placeholder text for the common name (CN) field of S/MIME certificates. If true, then the common name (CN) field of S/MIME certificates will be prefilled with information gathered from the system, e.g., from the email settings of the desktop or, on Windows, from the Active Directory. true - - This text will be used as placeholder text for the email address field of OpenPGP and S/MIME certificates. + + This text will be shown above the email address field of OpenPGP certificates and as placeholder in that field for S/MIME certificates. If true, then the email address field of OpenPGP and S/MIME certificates will be prefilled with information gathered from the system, e.g., from the email settings of the desktop or, on Windows, from the Active Directory. true + + + Prefilled value for the email address field of OpenPGP and S/MIME certificates. This will override EMAIL_prefill. It is useful if no or unsuitable system settings are found for EMAIL_prefill. + + - - This text will be used as placeholder text for the name field of OpenPGP certificates. + + This text will be shown above the name field of OpenPGP certificates. If true, then the name field of OpenPGP certificates will be prefilled with information gathered from the system, e.g., from the email settings of the desktop or, on Windows, from the Active Directory. true + + + Prefilled value for the name field of OpenPGP certificates. This will override NAME_prefill. It is useful if no or an unsuitable system setting is found for NAME_prefill. + + Specifies the default validity period of new OpenPGP keys in days. This setting specifies how many days a new OpenPGP key is valid by default, or, in other words, after how many days the key will expire. Set this to 0 for unlimited validity. If this setting is not set or if it is set to a negative value, then new OpenPGP keys will be valid for three years (possibly clamped to the allowed minimum or maximum validity period) by default. -1 Specifies the minimum validity period of new OpenPGP keys in days. This setting specifies how many days a new OpenPGP key is valid at least, or, in other words, after how many days the new key will expire at the earliest. 1 Specifies the maximum validity period of new OpenPGP keys in days. This setting specifies how many days a new OpenPGP key is valid at most, or, in other words, after how many days the new key will expire at the latest. If this setting is not set or if it is set to a negative value, then unlimited validity is allowed. -1 If true, hides the advanced settings button in the new certificate wizard. false Specifies the default validity of certifications in days. This setting specifies how many days a certification is valid by default, or, in other words, after how many days a new certification will expire. Set this to 0 for unlimited validity of certifications. 0 sha256sum If true, then the results are shown after successfully signing the clipboard. true If true, then the results are shown after successfully encrypting the clipboard. true Enables support for S/MIME (CMS). If false, then Kleopatra's main UI will not offer any functionality related to S/MIME (CMS). true Allows the creation of S/MIME certificate signing requests. If false, then Kleopatra will not offer the creation of S/MIME certificate signing requests. true Allows signing of text or files with S/MIME certificates. If false, then Kleopatra will not offer functionality for creating signatures with S/MIME certificates. true true true true true true true Specifies the display order of the DN attributes of X.509 certificates. Enable usage of groups of keys. Enable usage of groups of keys to create lists of recipients. true If enabled, then Kleopatra will automatically try to retrieve the keys that were used to certify the user ids of newly imported OpenPGP keys. This is useful in combination with trusted introducers. false By default, Kleopatra only queries the certificate directories of providers (WKD) for user IDs that were originally retrieved from a WKD when you update an OpenPGP certificate. If this option is enabled, then Kleopatra will query WKDs for all user IDs. false If enabled, then Kleopatra will show notifications in some place when using certificates that are about to expire soon. true This is a list of URL schemes that shall be blocked by the application. This can be used to prevent the application from opening external applications for certain URLs. Searches for the certificates belonging the smartcard keys on the configured keyserver. Searches on keyservers regardless of the protocol for the smartcards key, regardless of the keyserver protocol. Default behavior is to only do this for LDAP keyservers. false Automatically load S/MIME certificates from PKCS#15 (CardOS) smartcards If true, then Kleopatra will call gpgsm --learn if a PKCS#15 Smartcard is inserted with unknown certificates. This can take a while and blocks the smartcard while the command is running. true false diff --git a/src/libkleopatraclient/gui/certificaterequester.cpp b/src/libkleopatraclient/gui/certificaterequester.cpp index 11088cae2..6cd1b57ef 100644 --- a/src/libkleopatraclient/gui/certificaterequester.cpp +++ b/src/libkleopatraclient/gui/certificaterequester.cpp @@ -1,251 +1,251 @@ /* -*- mode: c++; c-basic-offset:4 -*- - gui/certificaterequester.h + gui/certificaterequester.cpp This file is part of KleopatraClient, the Kleopatra interface library SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: LGPL-2.0-or-later */ #include "certificaterequester.h" #include #include #include #include #include #include #include #include using namespace KleopatraClientCopy; using namespace KleopatraClientCopy::Gui; class CertificateRequester::Private { friend class ::KleopatraClientCopy::Gui::CertificateRequester; CertificateRequester *const q; public: explicit Private(CertificateRequester *qq) : q(qq) , selectedCertificates() , command() , multipleCertificatesAllowed(false) , onlySigningCertificatesAllowed(false) , onlyEncryptionCertificatesAllowed(false) , onlyOpenPGPCertificatesAllowed(false) , onlyX509CertificatesAllowed(false) , onlySecretKeysAllowed(false) , ui(q) { } private: void updateLineEdit() { ui.lineEdit.setText(selectedCertificates.join(QLatin1Char(' '))); } void createCommand() { std::unique_ptr cmd(new SelectCertificateCommand); cmd->setMultipleCertificatesAllowed(multipleCertificatesAllowed); cmd->setOnlySigningCertificatesAllowed(onlySigningCertificatesAllowed); cmd->setOnlyEncryptionCertificatesAllowed(onlyEncryptionCertificatesAllowed); cmd->setOnlyOpenPGPCertificatesAllowed(onlyOpenPGPCertificatesAllowed); cmd->setOnlyX509CertificatesAllowed(onlyX509CertificatesAllowed); cmd->setOnlySecretKeysAllowed(onlySecretKeysAllowed); cmd->setSelectedCertificates(selectedCertificates); if (const QWidget *const window = q->window()) { cmd->setParentWId(window->effectiveWinId()); } connect(cmd.get(), SIGNAL(finished()), q, SLOT(slotCommandFinished())); command = cmd.release(); } void slotButtonClicked(); void slotCommandFinished(); private: QStringList selectedCertificates; QPointer command; bool multipleCertificatesAllowed : 1; bool onlySigningCertificatesAllowed : 1; bool onlyEncryptionCertificatesAllowed : 1; bool onlyOpenPGPCertificatesAllowed : 1; bool onlyX509CertificatesAllowed : 1; bool onlySecretKeysAllowed : 1; struct Ui { QLineEdit lineEdit; QPushButton button; QHBoxLayout hlay; explicit Ui(CertificateRequester *qq) : lineEdit(qq) , button(i18n("Change..."), qq) , hlay(qq) { lineEdit.setObjectName(QLatin1StringView("lineEdit")); button.setObjectName(QLatin1StringView("button")); hlay.setObjectName(QLatin1StringView("hlay")); hlay.addWidget(&lineEdit, 1); hlay.addWidget(&button); lineEdit.setReadOnly(true); connect(&button, SIGNAL(clicked()), qq, SLOT(slotButtonClicked())); } } ui; }; CertificateRequester::CertificateRequester(QWidget *p, Qt::WindowFlags f) : QWidget(p, f) , d(new Private(this)) { } CertificateRequester::~CertificateRequester() { delete d; d = nullptr; } void CertificateRequester::setMultipleCertificatesAllowed(bool allow) { if (allow == d->multipleCertificatesAllowed) { return; } d->multipleCertificatesAllowed = allow; } bool CertificateRequester::multipleCertificatesAllowed() const { return d->multipleCertificatesAllowed; } void CertificateRequester::setOnlySigningCertificatesAllowed(bool allow) { if (allow == d->onlySigningCertificatesAllowed) { return; } d->onlySigningCertificatesAllowed = allow; } bool CertificateRequester::onlySigningCertificatesAllowed() const { return d->onlySigningCertificatesAllowed; } void CertificateRequester::setOnlyEncryptionCertificatesAllowed(bool allow) { if (allow == d->onlyEncryptionCertificatesAllowed) { return; } d->onlyEncryptionCertificatesAllowed = allow; } bool CertificateRequester::onlyEncryptionCertificatesAllowed() const { return d->onlyEncryptionCertificatesAllowed; } void CertificateRequester::setOnlyOpenPGPCertificatesAllowed(bool allow) { if (allow == d->onlyOpenPGPCertificatesAllowed) { return; } d->onlyOpenPGPCertificatesAllowed = allow; } bool CertificateRequester::onlyOpenPGPCertificatesAllowed() const { return d->onlyOpenPGPCertificatesAllowed; } void CertificateRequester::setOnlyX509CertificatesAllowed(bool allow) { if (allow == d->onlyX509CertificatesAllowed) { return; } d->onlyX509CertificatesAllowed = allow; } bool CertificateRequester::onlyX509CertificatesAllowed() const { return d->onlyX509CertificatesAllowed; } void CertificateRequester::setOnlySecretKeysAllowed(bool allow) { if (allow == d->onlySecretKeysAllowed) { return; } d->onlySecretKeysAllowed = allow; } bool CertificateRequester::onlySecretKeysAllowed() const { return d->onlySecretKeysAllowed; } void CertificateRequester::setSelectedCertificates(const QStringList &certs) { if (certs == d->selectedCertificates) { return; } d->selectedCertificates = certs; d->updateLineEdit(); Q_EMIT selectedCertificatesChanged(certs); } QStringList CertificateRequester::selectedCertificates() const { return d->selectedCertificates; } void CertificateRequester::setSelectedCertificate(const QString &cert) { setSelectedCertificates(QStringList(cert)); } QString CertificateRequester::selectedCertificate() const { return d->selectedCertificates.empty() ? QString() : d->selectedCertificates.front(); } void CertificateRequester::Private::slotButtonClicked() { if (command) { return; } createCommand(); command->start(); ui.button.setEnabled(false); } void CertificateRequester::Private::slotCommandFinished() { if (command->wasCanceled()) { /* do nothing */; } else if (command->error()) { QMessageBox::information(q, i18n("Kleopatra Error"), i18n("There was an error while connecting to Kleopatra: %1", command->errorString())); } else { q->setSelectedCertificates(command->selectedCertificates()); } ui.button.setEnabled(true); delete command; } #include "moc_certificaterequester.cpp" diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 74c324e75..97ec30505 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,909 +1,918 @@ /* -*- mode: c++; c-basic-offset:4 -*- mainwindow.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "aboutdata.h" #include "kleopatraapplication.h" #include "mainwindow.h" #include "settings.h" #include #include "view/keycacheoverlay.h" #include "view/keylistcontroller.h" #include "view/padwidget.h" #include "view/searchbar.h" #include "view/smartcardwidget.h" #include "view/tabwidget.h" #include "view/welcomewidget.h" #include "commands/decryptverifyfilescommand.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/importcrlcommand.h" #include "commands/selftestcommand.h" #include "commands/signencryptfilescommand.h" #include "conf/groupsconfigdialog.h" #include "utils/action_data.h" #include "utils/clipboardmenu.h" #include "utils/detail_p.h" #include "utils/filedialog.h" #include "utils/gui-helper.h" #include #include "dialogs/updatenotification.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 #include #include #include #include #include #include #include #include #include using namespace std::chrono_literals; using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; static KGuiItem KStandardGuiItem_quit() { static const QString app = KAboutData::applicationData().displayName(); KGuiItem item = KStandardGuiItem::quit(); item.setText(xi18nc("@action:button", "&Quit %1", app)); return item; } static KGuiItem KStandardGuiItem_close() { KGuiItem item = KStandardGuiItem::close(); item.setText(i18nc("@action:button", "Only &Close Window")); return item; } static bool isQuitting = false; namespace { static const std::vector mainViewActionNames = { QStringLiteral("view_certificate_overview"), QStringLiteral("manage_smartcard"), QStringLiteral("pad_view"), }; class CertificateView : public QWidget, public FocusFirstChild { Q_OBJECT public: CertificateView(QWidget *parent = nullptr) : QWidget{parent} , ui{this} { } SearchBar *searchBar() const { return ui.searchBar; } TabWidget *tabWidget() const { return ui.tabWidget; } void focusFirstChild(Qt::FocusReason reason) override { ui.searchBar->lineEdit()->setFocus(reason); } private: struct UI { TabWidget *tabWidget = nullptr; SearchBar *searchBar = nullptr; explicit UI(CertificateView *q) { auto vbox = new QVBoxLayout{q}; vbox->setSpacing(0); searchBar = new SearchBar{q}; vbox->addWidget(searchBar); tabWidget = new TabWidget{q}; vbox->addWidget(tabWidget); tabWidget->connectSearchBar(searchBar); } } ui; }; } class MainWindow::Private { friend class ::MainWindow; MainWindow *const q; public: explicit Private(MainWindow *qq); ~Private(); template void createAndStart() { (new T(this->currentView(), &this->controller))->start(); } template void createAndStart(QAbstractItemView *view) { (new T(view, &this->controller))->start(); } template void createAndStart(const QStringList &a) { (new T(a, this->currentView(), &this->controller))->start(); } template void createAndStart(const QStringList &a, QAbstractItemView *view) { (new T(a, view, &this->controller))->start(); } void closeAndQuit() { const QString app = KAboutData::applicationData().displayName(); const int rc = KMessageBox::questionTwoActionsCancel(q, xi18n("%1 may be used by other applications as a service." "You may instead want to close this window without exiting %1.", app), i18nc("@title:window", "Really Quit?"), KStandardGuiItem_close(), KStandardGuiItem_quit(), KStandardGuiItem::cancel(), QLatin1String("really-quit-") + app.toLower()); if (rc == KMessageBox::Cancel) { return; } isQuitting = true; if (!q->close()) { return; } // WARNING: 'this' might be deleted at this point! if (rc == KMessageBox::ButtonCode::SecondaryAction) { qApp->quit(); } } void configureToolbars() { KEditToolBar dlg(q->factory()); dlg.exec(); } void editKeybindings() { KShortcutsDialog::showDialog(q->actionCollection(), KShortcutsEditor::LetterShortcutsAllowed, q); updateSearchBarClickMessage(); } void updateSearchBarClickMessage() { const QString shortcutStr = focusToClickSearchAction->shortcut().toString(); ui.searchTab->searchBar()->updateClickMessage(shortcutStr); } void updateStatusBar() { auto statusBar = std::make_unique(); auto settings = KleopatraApplication::instance()->distributionSettings(); bool showStatusbar = false; if (settings) { const QString statusline = settings->value(QStringLiteral("statusline"), {}).toString(); if (!statusline.isEmpty()) { auto customStatusLbl = new QLabel(statusline); statusBar->insertWidget(0, customStatusLbl); showStatusbar = true; } } if (DeVSCompliance::isActive()) { auto statusLbl = std::make_unique(DeVSCompliance::name()); if (!SystemInfo::isHighContrastModeActive()) { const auto color = KColorScheme(QPalette::Active, KColorScheme::View) .foreground(DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText) .color(); const auto background = KColorScheme(QPalette::Active, KColorScheme::View) .background(DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground) .color(); statusLbl->setStyleSheet(QStringLiteral("QLabel { color: %1; background-color: %2; }").arg(color.name()).arg(background.name())); } statusBar->insertPermanentWidget(0, statusLbl.release()); showStatusbar = true; } if (showStatusbar) { q->setStatusBar(statusBar.release()); // QMainWindow takes ownership } else { q->setStatusBar(nullptr); } } void selfTest() { createAndStart(); } void configureGroups() { if (KConfigDialog::showDialog(GroupsConfigDialog::dialogName())) { return; } KConfigDialog *dialog = new GroupsConfigDialog(q); dialog->show(); } void showHandbook(); void gnupgLogViewer() { // Warning: Don't assume that the program needs to be in PATH. On Windows, it will also be found next to the calling process. if (!QProcess::startDetached(QStringLiteral("kwatchgnupg"), QStringList())) KMessageBox::error(q, i18n("Could not start the GnuPG Log Viewer (kwatchgnupg). " "Please check your installation."), i18n("Error Starting KWatchGnuPG")); } void forceUpdateCheck() { UpdateNotification::forceUpdateCheck(q); } void slotConfigCommitted(); void slotContextMenuRequested(QAbstractItemView *, const QPoint &p) { if (auto const menu = qobject_cast(q->factory()->container(QStringLiteral("listview_popup"), q))) { menu->exec(p); } else { qCDebug(KLEOPATRA_LOG) << "no \"listview_popup\" in kleopatra's ui.rc file"; } } void slotFocusQuickSearch() { ui.searchTab->searchBar()->lineEdit()->setFocus(); } void showView(const QString &actionName, QWidget *widget) { const auto coll = q->actionCollection(); if (coll) { for (const QString &name : mainViewActionNames) { if (auto action = coll->action(name)) { action->setChecked(name == actionName); } } } ui.stackWidget->setCurrentWidget(widget); if (auto ffci = dynamic_cast(widget)) { ffci->focusFirstChild(Qt::TabFocusReason); } } void showCertificateView() { if (KeyCache::instance()->keys().empty()) { showView(QStringLiteral("view_certificate_overview"), ui.welcomeWidget); } else { showView(QStringLiteral("view_certificate_overview"), ui.searchTab); } } void showSmartcardView() { showView(QStringLiteral("manage_smartcard"), ui.scWidget); } void showPadView() { if (!ui.padWidget) { ui.padWidget = new PadWidget; ui.stackWidget->addWidget(ui.padWidget); } showView(QStringLiteral("pad_view"), ui.padWidget); ui.stackWidget->resize(ui.padWidget->sizeHint()); } void restartDaemons() { Kleo::killDaemons(); } private: void setupActions(); QAbstractItemView *currentView() const { return ui.searchTab->tabWidget()->currentView(); } void keyListingDone() { const auto curWidget = ui.stackWidget->currentWidget(); if (curWidget == ui.scWidget || curWidget == ui.padWidget) { return; } showCertificateView(); } private: Kleo::KeyListController controller; bool firstShow : 1; struct UI { CertificateView *searchTab = nullptr; PadWidget *padWidget = nullptr; SmartCardWidget *scWidget = nullptr; WelcomeWidget *welcomeWidget = nullptr; QStackedWidget *stackWidget = nullptr; explicit UI(MainWindow *q); } ui; QAction *focusToClickSearchAction = nullptr; ClipboardMenu *clipboadMenu = nullptr; }; MainWindow::Private::UI::UI(MainWindow *q) : padWidget(nullptr) { auto mainWidget = new QWidget{q}; auto mainLayout = new QVBoxLayout(mainWidget); mainLayout->setContentsMargins({}); stackWidget = new QStackedWidget{q}; searchTab = new CertificateView{q}; stackWidget->addWidget(searchTab); new KeyCacheOverlay(mainWidget, q); scWidget = new SmartCardWidget{q}; stackWidget->addWidget(scWidget); welcomeWidget = new WelcomeWidget{q}; stackWidget->addWidget(welcomeWidget); mainLayout->addWidget(stackWidget); q->setCentralWidget(mainWidget); } MainWindow::Private::Private(MainWindow *qq) : q(qq) , controller(q) , firstShow(true) , ui(q) { KDAB_SET_OBJECT_NAME(controller); AbstractKeyListModel *flatModel = AbstractKeyListModel::createFlatKeyListModel(q); - flatModel->useKeyCache(true, KeyList::AllKeys); AbstractKeyListModel *hierarchicalModel = AbstractKeyListModel::createHierarchicalKeyListModel(q); - hierarchicalModel->useKeyCache(true, KeyList::AllKeys); KDAB_SET_OBJECT_NAME(flatModel); KDAB_SET_OBJECT_NAME(hierarchicalModel); controller.setFlatModel(flatModel); controller.setHierarchicalModel(hierarchicalModel); controller.setTabWidget(ui.searchTab->tabWidget()); ui.searchTab->tabWidget()->setFlatModel(flatModel); ui.searchTab->tabWidget()->setHierarchicalModel(hierarchicalModel); setupActions(); ui.stackWidget->setCurrentWidget(ui.searchTab); if (auto action = q->actionCollection()->action(QStringLiteral("view_certificate_overview"))) { action->setChecked(true); } connect(&controller, SIGNAL(contextMenuRequested(QAbstractItemView *, QPoint)), q, SLOT(slotContextMenuRequested(QAbstractItemView *, QPoint))); connect(KeyCache::instance().get(), &KeyCache::keyListingDone, q, [this]() { keyListingDone(); }); q->createGUI(QStringLiteral("kleopatra.rc")); // make toolbar buttons accessible by keyboard auto toolbar = q->findChild(); if (toolbar) { auto toolbarButtons = toolbar->findChildren(); for (auto b : toolbarButtons) { b->setFocusPolicy(Qt::TabFocus); } // move toolbar and its child widgets before the central widget in the tab order; // this is necessary to make Shift+Tab work as expected forceSetTabOrder(q, toolbar); auto toolbarChildren = toolbar->findChildren(); std::for_each(std::rbegin(toolbarChildren), std::rend(toolbarChildren), [toolbar](auto w) { forceSetTabOrder(toolbar, w); }); } if (auto action = q->actionCollection()->action(QStringLiteral("help_whats_this"))) { delete action; } q->setAcceptDrops(true); // set default window size q->resize(QSize(1024, 500)); q->setAutoSaveSettings(); updateSearchBarClickMessage(); updateStatusBar(); if (KeyCache::instance()->initialized()) { keyListingDone(); } + + // delay setting the models to use the key cache so that the UI (including + // the "Loading certificate cache..." overlay) is shown before the + // blocking key cache initialization happens + QMetaObject::invokeMethod( + q, + [flatModel, hierarchicalModel]() { + flatModel->useKeyCache(true, KeyList::AllKeys); + hierarchicalModel->useKeyCache(true, KeyList::AllKeys); + }, + Qt::QueuedConnection); } MainWindow::Private::~Private() { } MainWindow::MainWindow(QWidget *parent, Qt::WindowFlags flags) : KXmlGuiWindow(parent, flags) , d(new Private(this)) { } MainWindow::~MainWindow() { } void MainWindow::Private::setupActions() { KActionCollection *const coll = q->actionCollection(); const std::vector action_data = { // see keylistcontroller.cpp for more actions // Tools menu #ifndef Q_OS_WIN { "tools_start_kwatchgnupg", i18n("GnuPG Log Viewer"), QString(), "kwatchgnupg", q, [this](bool) { gnupgLogViewer(); }, QString(), }, #endif { "tools_restart_backend", i18nc("@action:inmenu", "Restart Background Processes"), i18nc("@info:tooltip", "Restart the background processes, e.g. after making changes to the configuration."), "view-refresh", q, [this](bool) { restartDaemons(); }, {}, }, // Help menu #ifdef Q_OS_WIN { "help_check_updates", i18n("Check for updates"), QString(), "gpg4win-compact", q, [this](bool) { forceUpdateCheck(); }, QString(), }, #endif // View menu { "view_certificate_overview", i18nc("@action show certificate overview", "Certificates"), i18n("Show certificate overview"), "view-certificate", q, [this](bool) { showCertificateView(); }, QString(), }, { "pad_view", i18nc("@action show input / output area for encrypting/signing resp. decrypting/verifying text", "Notepad"), i18n("Show pad for encrypting/decrypting and signing/verifying text"), "note", q, [this](bool) { showPadView(); }, QString(), }, { "manage_smartcard", i18nc("@action show smartcard management view", "Smartcards"), i18n("Show smartcard management"), "auth-sim-locked", q, [this](bool) { showSmartcardView(); }, QString(), }, // Settings menu { "settings_self_test", i18n("Perform Self-Test"), QString(), nullptr, q, [this](bool) { selfTest(); }, QString(), }, { "configure_groups", i18n("Configure Groups..."), QString(), "group", q, [this](bool) { configureGroups(); }, QString(), }}; make_actions_from_data(action_data, coll); if (!Settings().groupsEnabled()) { if (auto action = coll->action(QStringLiteral("configure_groups"))) { delete action; } } for (const QString &name : mainViewActionNames) { if (auto action = coll->action(name)) { action->setCheckable(true); } } KStandardAction::close(q, SLOT(close()), coll); KStandardAction::quit(q, SLOT(closeAndQuit()), coll); KStandardAction::configureToolbars(q, SLOT(configureToolbars()), coll); KStandardAction::keyBindings(q, SLOT(editKeybindings()), coll); KStandardAction::preferences(qApp, SLOT(openOrRaiseConfigDialog()), coll); focusToClickSearchAction = new QAction(i18n("Set Focus to Quick Search"), q); coll->addAction(QStringLiteral("focus_to_quickseach"), focusToClickSearchAction); coll->setDefaultShortcut(focusToClickSearchAction, QKeySequence(Qt::ALT | Qt::Key_Q)); connect(focusToClickSearchAction, SIGNAL(triggered(bool)), q, SLOT(slotFocusQuickSearch())); clipboadMenu = new ClipboardMenu(q); clipboadMenu->setMainWindow(q); clipboadMenu->clipboardMenu()->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); clipboadMenu->clipboardMenu()->setPopupMode(QToolButton::InstantPopup); coll->addAction(QStringLiteral("clipboard_menu"), clipboadMenu->clipboardMenu()); /* Add additional help actions for documentation */ const auto compendium = new DocAction(QIcon::fromTheme(QStringLiteral("gpg4win-compact")), i18n("Gpg4win Compendium"), i18nc("The Gpg4win compendium is only available" "at this point (24.7.2017) in german and english." "Please check with Gpg4win before translating this filename.", "gpg4win-compendium-en.pdf"), QStringLiteral("../share/gpg4win"), coll); coll->addAction(QStringLiteral("help_doc_compendium"), compendium); /* Documentation centered around the german approved VS-NfD mode for official * RESTRICTED communication. This is only available in some distributions with * the focus on official communications. */ const auto quickguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Quickguide"), i18nc("Only available in German and English. Leave to English for other languages.", "encrypt_and_sign_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_quickguide"), quickguide); const auto symguide = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Password-based encryption"), i18nc("Only available in German and English. Leave to English for other languages.", "symmetric_encryption_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_symenc"), symguide); const auto groups = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Group configuration"), i18nc("Only available in German and English. Leave to English for other languages.", "groupfeature_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_groups"), groups); #ifdef Q_OS_WIN const auto gpgol = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Mail encryption in Outlook"), i18nc("Only available in German and English. Leave to English for other languages. Only shown on Windows.", "gpgol_outlook_addin_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_gpgol"), gpgol); #endif /* The submenu with advanced topics */ const auto certmngmnt = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Certification Management"), i18nc("Only available in German and English. Leave to English for other languages.", "certification_management_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_cert_management"), certmngmnt); const auto smartcard = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("&Smartcard setup"), i18nc("Only available in German and English. Leave to English for other languages.", "smartcard_setup_gnupgvsd_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_smartcard"), smartcard); const auto man_gnupg = new DocAction(QIcon::fromTheme(QStringLiteral("help-contextual")), i18n("GnuPG Command&line"), QStringLiteral("gnupg_manual_en.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_gnupg"), man_gnupg); /* The secops */ const auto vsa10573 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10573"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10573-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_vsa10573"), vsa10573); const auto vsa10584 = new DocAction(QIcon::fromTheme(QStringLiteral("dvipdf")), i18n("SecOps VSA-10584"), i18nc("Only available in German and English. Leave to English for other languages.", "BSI-VSA-10584-ENG_secops-20220207.pdf"), QStringLiteral("../share/doc/gnupg-vsd"), coll); coll->addAction(QStringLiteral("help_doc_vsa10584"), vsa10584); q->setStandardToolBarMenuEnabled(true); controller.createActions(coll); ui.searchTab->tabWidget()->createActions(coll); } void MainWindow::Private::slotConfigCommitted() { controller.updateConfig(); updateStatusBar(); } void MainWindow::closeEvent(QCloseEvent *e) { // KMainWindow::closeEvent() insists on quitting the application, // so do not let it touch the event... qCDebug(KLEOPATRA_LOG); if (d->controller.hasRunningCommands()) { if (d->controller.shutdownWarningRequired()) { const int ret = KMessageBox::warningContinueCancel(this, i18n("There are still some background operations ongoing. " "These will be terminated when closing the window. " "Proceed?"), i18n("Ongoing Background Tasks")); if (ret != KMessageBox::Continue) { e->ignore(); return; } } d->controller.cancelCommands(); if (d->controller.hasRunningCommands()) { // wait for them to be finished: setEnabled(false); QEventLoop ev; QTimer::singleShot(100ms, &ev, &QEventLoop::quit); connect(&d->controller, &KeyListController::commandsExecuting, &ev, &QEventLoop::quit); ev.exec(); if (d->controller.hasRunningCommands()) qCWarning(KLEOPATRA_LOG) << "controller still has commands running, this may crash now..."; setEnabled(true); } } if (isQuitting || qApp->isSavingSession()) { d->ui.searchTab->tabWidget()->saveViews(KSharedConfig::openConfig().data()); KConfigGroup grp(KConfigGroup(KSharedConfig::openConfig(), autoSaveGroup())); saveMainWindowSettings(grp); e->accept(); } else { e->ignore(); hide(); } } void MainWindow::showEvent(QShowEvent *e) { KXmlGuiWindow::showEvent(e); if (d->firstShow) { d->ui.searchTab->tabWidget()->loadViews(KSharedConfig::openConfig().data()); d->firstShow = false; } if (!savedGeometry.isEmpty()) { restoreGeometry(savedGeometry); } } void MainWindow::hideEvent(QHideEvent *e) { savedGeometry = saveGeometry(); KXmlGuiWindow::hideEvent(e); } void MainWindow::importCertificatesFromFile(const QStringList &files) { if (!files.empty()) { d->createAndStart(files); } } static QStringList extract_local_files(const QMimeData *data) { const QList urls = data->urls(); // begin workaround KDE/Qt misinterpretation of text/uri-list QList::const_iterator end = urls.end(); if (urls.size() > 1 && !urls.back().isValid()) { --end; } // end workaround QStringList result; std::transform(urls.begin(), end, std::back_inserter(result), std::mem_fn(&QUrl::toLocalFile)); result.erase(std::remove_if(result.begin(), result.end(), std::mem_fn(&QString::isEmpty)), result.end()); return result; } static bool can_decode_local_files(const QMimeData *data) { if (!data) { return false; } return !extract_local_files(data).empty(); } void MainWindow::dragEnterEvent(QDragEnterEvent *e) { qCDebug(KLEOPATRA_LOG); if (can_decode_local_files(e->mimeData())) { e->acceptProposedAction(); } } void MainWindow::dropEvent(QDropEvent *e) { qCDebug(KLEOPATRA_LOG); if (!can_decode_local_files(e->mimeData())) { return; } e->setDropAction(Qt::CopyAction); const QStringList files = extract_local_files(e->mimeData()); const unsigned int classification = classify(files); QMenu menu; QAction *const signEncrypt = menu.addAction(i18n("Sign/Encrypt...")); QAction *const decryptVerify = mayBeAnyMessageType(classification) ? menu.addAction(i18n("Decrypt/Verify...")) : nullptr; if (signEncrypt || decryptVerify) { menu.addSeparator(); } QAction *const importCerts = mayBeAnyCertStoreType(classification) ? menu.addAction(i18n("Import Certificates")) : nullptr; QAction *const importCRLs = mayBeCertificateRevocationList(classification) ? menu.addAction(i18n("Import CRLs")) : nullptr; if (importCerts || importCRLs) { menu.addSeparator(); } if (!signEncrypt && !decryptVerify && !importCerts && !importCRLs) { return; } menu.addAction(i18n("Cancel")); const QAction *const chosen = menu.exec(mapToGlobal(e->position().toPoint())); if (!chosen) { return; } if (chosen == signEncrypt) { d->createAndStart(files); } else if (chosen == decryptVerify) { d->createAndStart(files); } else if (chosen == importCerts) { d->createAndStart(files); } else if (chosen == importCRLs) { d->createAndStart(files); } e->accept(); } void MainWindow::readProperties(const KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::readProperties(cg); setHidden(cg.readEntry("hidden", false)); } void MainWindow::saveProperties(KConfigGroup &cg) { qCDebug(KLEOPATRA_LOG); KXmlGuiWindow::saveProperties(cg); cg.writeEntry("hidden", isHidden()); } #include "mainwindow.moc" #include "moc_mainwindow.cpp" diff --git a/src/smartcard/netkeycard.cpp b/src/smartcard/netkeycard.cpp index d880a067e..18cc6c48c 100644 --- a/src/smartcard/netkeycard.cpp +++ b/src/smartcard/netkeycard.cpp @@ -1,124 +1,155 @@ /* smartcard/netkeycard.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "netkeycard.h" #include "keypairinfo.h" #include "kleopatra_debug.h" +#include #include +#include #include #include #include #include #include using namespace Kleo; using namespace Kleo::SmartCard; // static const std::string NetKeyCard::AppName = "nks"; namespace { static GpgME::Key lookup_key(GpgME::Context *ctx, const std::string &keyGrip) { if (!ctx || keyGrip.empty()) { return GpgME::Key(); } const std::string pattern = '&' + keyGrip; qCDebug(KLEOPATRA_LOG) << "parse_keypairinfo_and_lookup_key: pattern=" << pattern.c_str(); if (const auto err = ctx->startKeyListing(pattern.c_str())) { qCDebug(KLEOPATRA_LOG) << "parse_keypairinfo_and_lookup_key: startKeyListing failed:" << Formatting::errorAsString(err); return GpgME::Key(); } GpgME::Error e; const auto key = ctx->nextKey(e); ctx->endKeyListing(); qCDebug(KLEOPATRA_LOG) << "parse_keypairinfo_and_lookup_key: e=" << e.code() << "; key.isNull()" << key.isNull(); return key; } } // namespace NetKeyCard::NetKeyCard(const Card &card) : Card(card) { setAppName(AppName); } // static std::string NetKeyCard::nksPinKeyRef() { return std::string("PW1.CH"); } // static std::string NetKeyCard::sigGPinKeyRef() { return std::string("PW1.CH.SIG"); } void NetKeyCard::processCardInfo() { setKeyPairInfo(keyInfos()); } void NetKeyCard::setKeyPairInfo(const std::vector &infos) { // check that any of the keys are new const std::unique_ptr klc(GpgME::Context::createForProtocol(GpgME::CMS)); if (!klc.get()) { return; } klc->setKeyListMode(GpgME::Ephemeral); klc->addKeyListMode(GpgME::Validate); setCanLearnKeys(false); mKeys.clear(); for (const auto &info : infos) { const auto key = lookup_key(klc.get(), info.grip); - mKeys.push_back(key); - } - if (mKeys.empty()) { - setCanLearnKeys(true); + if (key.isNull()) { + setCanLearnKeys(true); + } else { + mKeys.push_back(key); + } } } // State 0 -> NKS PIN Retry counter // State 1 -> NKS PUK Retry counter // State 2 -> SigG PIN Retry counter // State 3 -> SigG PUK Retry counter bool NetKeyCard::hasNKSNullPin() const { const auto states = pinStates(); if (states.size() < 2) { qCWarning(KLEOPATRA_LOG) << "Invalid size of pin states:" << states.size(); return false; } return states[0] == Card::NullPin; } bool NetKeyCard::hasSigGNullPin() const { const auto states = pinStates(); if (states.size() < 4) { qCWarning(KLEOPATRA_LOG) << "Invalid size of pin states:" << states.size(); return false; } return states[2] == Card::NullPin; } std::vector NetKeyCard::keys() const { return mKeys; } + +bool NetKeyCard::operator==(const Card &other) const +{ + static const _detail::ByFingerprint keysHaveSameFingerprint; + + if (!Card::operator==(other)) { + qCDebug(KLEOPATRA_LOG) << "NetKeyCard" << __func__ << "Card don't match"; + return false; + } + + const auto otherNetKeyCard = dynamic_cast(&other); + if (!otherNetKeyCard) { + qCWarning(KLEOPATRA_LOG) << "Failed to cast other card to NetKeyCard"; + return false; + } + if (mKeys.size() != otherNetKeyCard->mKeys.size()) { + qCDebug(KLEOPATRA_LOG) << "NetKeyCard" << __func__ << "Number of keys doesn't match"; + return false; + } + const auto otherHasKey = [otherNetKeyCard](const GpgME::Key &key) { + return Kleo::any_of(otherNetKeyCard->mKeys, [key](const GpgME::Key &otherKey) { + return keysHaveSameFingerprint(key, otherKey); + }); + }; + const bool result = Kleo::all_of(mKeys, otherHasKey); + qCDebug(KLEOPATRA_LOG) << "NetKeyCard" << __func__ << "Keys match?" << result; + return result; +} diff --git a/src/smartcard/netkeycard.h b/src/smartcard/netkeycard.h index 7ab834f96..ac28f4846 100644 --- a/src/smartcard/netkeycard.h +++ b/src/smartcard/netkeycard.h @@ -1,44 +1,46 @@ #pragma once /* smartcard/netkeycard.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "card.h" #include namespace Kleo { namespace SmartCard { struct KeyPairInfo; /** Class to work with NetKey smartcards or compatible tokens */ class NetKeyCard : public Card { public: explicit NetKeyCard(const Card &card); static const std::string AppName; static std::string nksPinKeyRef(); static std::string sigGPinKeyRef(); bool hasSigGNullPin() const; bool hasNKSNullPin() const; std::vector keys() const; private: void processCardInfo() override; void setKeyPairInfo(const std::vector &infos); + bool operator==(const Card &other) const override; + private: std::vector mKeys; }; } // namespace Smartcard } // namespace Kleopatra diff --git a/src/smartcard/openpgpcard.cpp b/src/smartcard/openpgpcard.cpp index beac4c75a..f5a18e05a 100644 --- a/src/smartcard/openpgpcard.cpp +++ b/src/smartcard/openpgpcard.cpp @@ -1,182 +1,170 @@ /* smartcard/openpgpcard.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 */ /* Code in this file is partly based on the GNU Privacy Assistant * (cm-openpgp.c) git rev. 0a78795146661234070681737b3e08228616441f * * Whis is: * SPDX-FileCopyrightText: 2008, 2009 g 10 Code GmbH * * And may be licensed under the GNU General Public License * as published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. */ #include "openpgpcard.h" #include "algorithminfo.h" #include +#include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::SmartCard; // static const std::string OpenPGPCard::AppName = "openpgp"; OpenPGPCard::OpenPGPCard(const Card &card) : Card(card) { setAppName(AppName); setInitialKeyInfos(OpenPGPCard::supportedKeys()); } // static std::string OpenPGPCard::pgpSigKeyRef() { return std::string("OPENPGP.1"); } // static std::string OpenPGPCard::pgpEncKeyRef() { return std::string("OPENPGP.2"); } // static std::string OpenPGPCard::pgpAuthKeyRef() { return std::string("OPENPGP.3"); } // static std::string OpenPGPCard::pinKeyRef() { return std::string("OPENPGP.1"); } // static std::string OpenPGPCard::adminPinKeyRef() { return std::string("OPENPGP.3"); } // static std::string OpenPGPCard::resetCodeKeyRef() { return std::string("OPENPGP.2"); } // static const std::vector &OpenPGPCard::supportedKeys() { static const std::vector keyInfos = { {OpenPGPCard::pgpSigKeyRef(), "", "sc", "", ""}, {OpenPGPCard::pgpEncKeyRef(), "", "e", "", ""}, {OpenPGPCard::pgpAuthKeyRef(), "", "a", "", ""}, }; return keyInfos; } // static QString OpenPGPCard::keyDisplayName(const std::string &keyRef) { static const QMap displayNames = { {OpenPGPCard::pgpSigKeyRef(), i18n("Signature")}, {OpenPGPCard::pgpEncKeyRef(), i18n("Encryption")}, {OpenPGPCard::pgpAuthKeyRef(), i18n("Authentication")}, }; return displayNames.value(keyRef); } // static std::string OpenPGPCard::getAlgorithmName(const std::string &algorithm, const std::string &keyRef) { static const std::map ecdhAlgorithmMapping = { {"curve25519", "cv25519"}, {"curve448", "cv448"}, }; static const std::map eddsaAlgorithmMapping = { {"curve25519", "ed25519"}, {"curve448", "ed448"}, }; if (keyRef == OpenPGPCard::pgpEncKeyRef()) { const auto it = ecdhAlgorithmMapping.find(algorithm); if (it != ecdhAlgorithmMapping.end()) { return it->second; } } else { const auto it = eddsaAlgorithmMapping.find(algorithm); if (it != eddsaAlgorithmMapping.end()) { return it->second; } } return algorithm; } void OpenPGPCard::setSupportedAlgorithms(const std::vector &algorithms) { const auto &availableAlgos = Kleo::availableAlgorithms(); const auto &ignoredAlgos = Kleo::ignoredAlgorithms(); mAlgorithms.clear(); Kleo::copy_if(algorithms, std::back_inserter(mAlgorithms), [&availableAlgos](const auto &algo) { return Kleo::contains(availableAlgos, algo); }); if (mAlgorithms.size() < algorithms.size()) { std::vector unsupportedAlgos; Kleo::copy_if(algorithms, std::back_inserter(unsupportedAlgos), [&availableAlgos, &ignoredAlgos](const auto &algo) { return !Kleo::contains(ignoredAlgos, algo) && !Kleo::contains(availableAlgos, algo); }); if (unsupportedAlgos.size() > 0) { qCWarning(KLEOPATRA_LOG).nospace() << "OpenPGPCard::" << __func__ << " Unsupported algorithms: " << unsupportedAlgos << " (supported: " << availableAlgos << ")"; } } } std::string OpenPGPCard::pubkeyUrl() const { return cardInfo("PUBKEY-URL"); } std::vector OpenPGPCard::supportedAlgorithms() const { - static const std::map displayNames = { - {"brainpoolP256r1", i18nc("@info", "ECC (Brainpool P-256)")}, - {"brainpoolP384r1", i18nc("@info", "ECC (Brainpool P-384)")}, - {"brainpoolP512r1", i18nc("@info", "ECC (Brainpool P-512)")}, - {"curve25519", i18nc("@info", "ECC (Curve25519)")}, - {"curve448", i18nc("@info", "ECC (Curve448)")}, - {"nistp256", i18nc("@info", "ECC (NIST P-256)")}, - {"nistp384", i18nc("@info", "ECC (NIST P-384)")}, - {"nistp521", i18nc("@info", "ECC (NIST P-521)")}, - {"rsa2048", i18nc("@info", "RSA 2048")}, - {"rsa3072", i18nc("@info", "RSA 3072")}, - {"rsa4096", i18nc("@info", "RSA 4096")}, - }; std::vector algos; std::transform(mAlgorithms.cbegin(), mAlgorithms.cend(), std::back_inserter(algos), [](const auto &algo) { - return AlgorithmInfo{algo, displayNames.at(algo)}; + return AlgorithmInfo{algo, Formatting::prettyAlgorithmName(algo)}; }); return algos; } bool OpenPGPCard::isSupportedAlgorithm(const std::string &algorithm) const { return Kleo::contains(mAlgorithms, algorithm); } diff --git a/src/uiserver/decryptcommand.h b/src/uiserver/decryptcommand.h index 72161659a..ad82f73b0 100644 --- a/src/uiserver/decryptcommand.h +++ b/src/uiserver/decryptcommand.h @@ -1,40 +1,40 @@ /* -*- mode: c++; c-basic-offset:4 -*- - uiserver/decryptemailcommand.h + uiserver/decryptcommand.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 namespace Kleo { class DecryptCommand : public AssuanCommandMixin { public: // DecryptCommand(); //~DecryptCommand(); private: DecryptVerifyOperation operation() const override { if (hasOption("no-verify")) { return Decrypt; } else { return DecryptVerify; } } public: static const char *staticName() { return "DECRYPT"; } }; } diff --git a/src/uiserver/decryptfilescommand.h b/src/uiserver/decryptfilescommand.h index 0e84ba3eb..d1c9dc1bf 100644 --- a/src/uiserver/decryptfilescommand.h +++ b/src/uiserver/decryptfilescommand.h @@ -1,36 +1,36 @@ /* -*- mode: c++; c-basic-offset:4 -*- - uiserver/decryptemailcommand.h + uiserver/decryptfilescommand.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 namespace Kleo { class DecryptFilesCommand : public AssuanCommandMixin { public: // DecryptFilesCommand(); //~DecryptFilesCommand(); private: DecryptVerifyOperation operation() const override { return Decrypt; } public: static const char *staticName() { return "DECRYPT_FILES"; } }; } diff --git a/src/uiserver/decryptverifycommandemailbase.h b/src/uiserver/decryptverifycommandemailbase.h index cefc8a4e0..f04244344 100644 --- a/src/uiserver/decryptverifycommandemailbase.h +++ b/src/uiserver/decryptverifycommandemailbase.h @@ -1,64 +1,64 @@ /* -*- mode: c++; c-basic-offset:4 -*- - uiserver/decryptverifycommand.h + uiserver/decryptverifycommandemailbase.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "assuancommand.h" #include #include namespace Kleo { class DecryptVerifyCommandEMailBase : public AssuanCommandMixin { public: explicit DecryptVerifyCommandEMailBase(); ~DecryptVerifyCommandEMailBase() override; private: virtual DecryptVerifyOperation operation() const = 0; virtual Mode mode() const { return EMail; } private: int doStart() override; void doCanceled() override; public: static const char *staticName() { return ""; } class Private; private: kdtools::pimpl_ptr d; }; class DecryptVerifyCommand : public AssuanCommandMixin { public: private: DecryptVerifyOperation operation() const override { return DecryptVerify; } public: static const char *staticName() { return "DECRYPT_VERIFY"; } }; } diff --git a/src/uiserver/decryptverifyfilescommand.h b/src/uiserver/decryptverifyfilescommand.h index b58e8be0b..cf576f515 100644 --- a/src/uiserver/decryptverifyfilescommand.h +++ b/src/uiserver/decryptverifyfilescommand.h @@ -1,36 +1,36 @@ /* -*- mode: c++; c-basic-offset:4 -*- - uiserver/decryptemailcommand.h + uiserver/decryptverifyfilescommand.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 namespace Kleo { class DecryptVerifyFilesCommand : public AssuanCommandMixin { public: // DecryptVerifyFilesCommand(); //~DecryptVerifyFilesCommand(); private: DecryptVerifyOperation operation() const override { return DecryptVerify; } public: static const char *staticName() { return "DECRYPT_VERIFY_FILES"; } }; } diff --git a/src/utils/path-helper.cpp b/src/utils/path-helper.cpp index 997e4e6f2..f8117279b 100644 --- a/src/utils/path-helper.cpp +++ b/src/utils/path-helper.cpp @@ -1,216 +1,195 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/path-helper.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "path-helper.h" #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include - -#include - #ifdef Q_OS_WIN -extern Q_CORE_EXPORT int qt_ntfs_permission_lookup; +#include #endif +#include + using namespace Kleo; static QString commonPrefix(const QString &s1, const QString &s2) { return QString(s1.data(), std::mismatch(s1.data(), s1.data() + std::min(s1.size(), s2.size()), s2.data()).first - s1.data()); } static QString longestCommonPrefix(const QStringList &sl) { if (sl.empty()) { return QString(); } QString result = sl.front(); for (const QString &s : sl) { result = commonPrefix(s, result); } return result; } QString Kleo::heuristicBaseDirectory(const QStringList &fileNames) { QStringList dirs; for (const QString &fileName : fileNames) { dirs.push_back(QFileInfo(fileName).path() + QLatin1Char('/')); } qCDebug(KLEOPATRA_LOG) << "dirs" << dirs; const QString candidate = longestCommonPrefix(dirs); /* Special case handling for Outlook and KMail attachment temporary path. * This is otherwise something like: * c:\users\username\AppData\Local\Microsoft\Windows\INetCache\ * Content.Outlook\ADSDFG9\foo.txt * * For KMail it is usually /tmp/messageviewer/foo * * Both are paths that are unlikely to be the target path to save the * decrypted attachment. * * This is very common when encrypted attachments are opened * within Outlook or KMail. */ if (candidate.contains(QStringLiteral("Content.Outlook")) // || candidate.contains(QStringLiteral("messageviewer"))) { return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } const int idx = candidate.lastIndexOf(QLatin1Char('/')); return candidate.left(idx); } QStringList Kleo::makeRelativeTo(const QString &base, const QStringList &fileNames) { if (base.isEmpty()) { return fileNames; } else { return makeRelativeTo(QDir(base), fileNames); } } QStringList Kleo::makeRelativeTo(const QDir &baseDir, const QStringList &fileNames) { QStringList rv; rv.reserve(fileNames.size()); std::transform(fileNames.cbegin(), fileNames.cend(), std::back_inserter(rv), [&baseDir](const QString &file) { return baseDir.relativeFilePath(file); }); return rv; } QString Kleo::stripSuffix(const QString &fileName) { const QFileInfo fi(fileName); return fi.dir().filePath(fi.completeBaseName()); } -#ifdef Q_OS_WIN -namespace -{ -class NTFSPermissionsCheck -{ -public: - NTFSPermissionsCheck() - { - // enable the NTFS permissions check - qt_ntfs_permission_lookup++; - qCDebug(KLEOPATRA_LOG) << __func__ << "NTFS permissions check" << (qt_ntfs_permission_lookup ? "enabled" : "disabled"); - } - - ~NTFSPermissionsCheck() - { - // disable the NTFS permissions check - qt_ntfs_permission_lookup--; - qCDebug(KLEOPATRA_LOG) << __func__ << "NTFS permissions check" << (qt_ntfs_permission_lookup ? "enabled" : "disabled"); - } -}; -} -#endif - bool Kleo::isWritable(const QFileInfo &fi) { #ifdef Q_OS_WIN - NTFSPermissionsCheck withExpensiveNTFSPermissionsCheck; - const auto result = fi.isWritable(); - qCDebug(KLEOPATRA_LOG) << __func__ << fi.absoluteFilePath() << (result ? "is writable" : "is not writable"); - return result; -#else - return fi.isWritable(); + if (fi.isDir()) { + QTemporaryFile dummy{fi.absoluteFilePath() + QLatin1String{"/tempXXXXXX"}}; + const auto fileCreated = dummy.open(); + if (!fileCreated) { + qCDebug(KLEOPATRA_LOG) << "Failed to create test file in folder" << fi.absoluteFilePath(); + } + return fileCreated; + } #endif + return fi.isWritable(); } #ifdef Q_OS_WIN void Kleo::recursivelyRemovePath(const QString &path) { const QFileInfo fi(path); if (fi.isDir()) { QDir dir(path); const auto dirs{dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden)}; for (const QString &fname : dirs) { recursivelyRemovePath(dir.filePath(fname)); } const QString dirName = fi.fileName(); dir.cdUp(); if (!dir.rmdir(dirName)) { throw Exception(GPG_ERR_EPERM, i18n("Cannot remove directory %1", path)); } } else { QFile file(path); if (!file.remove()) { throw Exception(GPG_ERR_EPERM, i18n("Cannot remove file %1: %2", path, file.errorString())); } } } bool Kleo::recursivelyCopy(const QString &src, const QString &dest) { QDir srcDir(src); if (!srcDir.exists()) { return false; } QDir destDir(dest); if (!destDir.exists() && !destDir.mkdir(dest)) { return false; } for (const auto &file : srcDir.entryList(QDir::Files | QDir::Hidden)) { const QString srcName = src + QLatin1Char('/') + file; const QString destName = dest + QLatin1Char('/') + file; if (!QFile::copy(srcName, destName)) { return false; } } for (const auto &dir : srcDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden)) { const QString srcName = src + QLatin1Char('/') + dir; const QString destName = dest + QLatin1Char('/') + dir; if (!recursivelyCopy(srcName, destName)) { return false; } } return true; } bool Kleo::moveDir(const QString &src, const QString &dest) { // Need an existing path to query the device const QString parentDest = QFileInfo(dest).dir().absolutePath(); const auto srcDevice = QStorageInfo(src).device(); if (!srcDevice.isEmpty() // && srcDevice == QStorageInfo(parentDest).device() // && QFile::rename(src, dest)) { qCDebug(KLEOPATRA_LOG) << "Renamed" << src << "to" << dest; return true; } // first copy if (!recursivelyCopy(src, dest)) { return false; } // Then delete original recursivelyRemovePath(src); return true; } #endif diff --git a/src/utils/path-helper.h b/src/utils/path-helper.h index f2f645aa2..e46b6883a 100644 --- a/src/utils/path-helper.h +++ b/src/utils/path-helper.h @@ -1,41 +1,40 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/path-helper.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include class QString; #include class QDir; class QFileInfo; namespace Kleo { QString heuristicBaseDirectory(const QStringList &files); QStringList makeRelativeTo(const QDir &dir, const QStringList &files); QStringList makeRelativeTo(const QString &dir, const QStringList &files); QString stripSuffix(const QString &fileName); /** * Checks if the file/directory referenced by \p fi is writable. * - * On Windows, this enables the NTFS permissions check which is an expensive - * operation. Only use this in the main thread. + * On Windows, a temporary file is created to check if a directory is writable. * \sa QFileInfo::isWritable */ bool isWritable(const QFileInfo &fi); #ifdef Q_OS_WIN void recursivelyRemovePath(const QString &path); bool recursivelyCopy(const QString &src, const QString &dest); bool moveDir(const QString &src, const QString &dest); #endif } diff --git a/src/view/keylistcontroller.cpp b/src/view/keylistcontroller.cpp index 0accb11df..1fceb1200 100644 --- a/src/view/keylistcontroller.cpp +++ b/src/view/keylistcontroller.cpp @@ -1,1011 +1,1011 @@ /* -*- mode: c++; c-basic-offset:4 -*- - controllers/keylistcontroller.cpp + 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/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; 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); } void addView(QAbstractItemView *view) { connectView(view); views.insert(std::lower_bound(views.begin(), views.end(), view), view); } void removeView(QAbstractItemView *view) { view->disconnect(q); view->selectionModel()->disconnect(q); views.erase(std::remove(views.begin(), views.end(), view), views.end()); } 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()); } 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)) { 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)) { return; } d->addView(view); } void KeyListController::removeView(QAbstractItemView *view) { if (!view || !std::binary_search(d->views.cbegin(), d->views.cend(), 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) { 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) { 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"), nullptr /*"folder-edit-sign-encrypt"*/, 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(), }, // 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)); } 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_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)) { 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)); } // slot void KeyListController::cancelCommands() { std::for_each(d->commands.begin(), d->commands.end(), 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)) { 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)) { 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)) { 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)) { 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; } } 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)) { 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(); })) { result |= Command::NeedSecretKeyData; } if (std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { return key.protocol() == OpenPGP; })) { result |= Command::MustBeOpenPGP; } else if (std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { return key.protocol() == CMS; })) { result |= Command::MustBeCMS; } if (Kleo::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; } if (rs->anyCardCanLearnKeys()) { result |= Command::AnyCardCanLearnKeys; } } return result; } void KeyListController::Private::slotActionTriggered(QAction *sender) { const auto it = std::find_if(actions.cbegin(), actions.cend(), [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/keylistcontroller.h b/src/view/keylistcontroller.h index b8f859f2b..beeb8729c 100644 --- a/src/view/keylistcontroller.h +++ b/src/view/keylistcontroller.h @@ -1,99 +1,99 @@ /* -*- mode: c++; c-basic-offset:4 -*- - controllers/keylistcontroller.h + view/keylistcontroller.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 #include #include class QAbstractItemView; class QAction; class QPoint; class QItemSelectionModel; class KActionCollection; namespace Kleo { class AbstractKeyListModel; class Command; class TabWidget; class KeyListController : public QObject { Q_OBJECT public: explicit KeyListController(QObject *parent = nullptr); ~KeyListController() override; std::vector views() const; void setFlatModel(AbstractKeyListModel *model); AbstractKeyListModel *flatModel() const; void setHierarchicalModel(AbstractKeyListModel *model); AbstractKeyListModel *hierarchicalModel() const; void setParentWidget(QWidget *parent); QWidget *parentWidget() const; QAbstractItemView *currentView() const; void setTabWidget(TabWidget *tabs); TabWidget *tabWidget() const; void registerCommand(Command *cmd); void createActions(KActionCollection *collection); template void registerActionForCommand(QAction *action) { this->registerAction(action, T_Command::restrictions(), &KeyListController::template create); } void enableDisableActions(const QItemSelectionModel *sm) const; bool hasRunningCommands() const; bool shutdownWarningRequired() const; private: void registerAction(QAction *action, Command::Restrictions restrictions, Command *(*create)(QAbstractItemView *, KeyListController *)); template static Command *create(QAbstractItemView *v, KeyListController *c) { return new T_Command(v, c); } public Q_SLOTS: void addView(QAbstractItemView *view); void removeView(QAbstractItemView *view); void setCurrentView(QAbstractItemView *view); void cancelCommands(); void updateConfig(); Q_SIGNALS: void progress(int current, int total); void commandsExecuting(bool); void contextMenuRequested(QAbstractItemView *view, const QPoint &p); private: class Private; kdtools::pimpl_ptr d; }; } diff --git a/src/view/netkeywidget.cpp b/src/view/netkeywidget.cpp index 07e540dec..d2beb3406 100644 --- a/src/view/netkeywidget.cpp +++ b/src/view/netkeywidget.cpp @@ -1,340 +1,340 @@ /* view/netkeywidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "netkeywidget.h" #include "keytreeview.h" #include "kleopatraapplication.h" #include "nullpinwidget.h" #include "systrayicon.h" #include "kleopatra_debug.h" #include "smartcard/netkeycard.h" #include "smartcard/readerstatus.h" #include "commands/changepincommand.h" #include "commands/createcsrforcardkeycommand.h" #include "commands/createopenpgpkeyfromcardkeyscommand.h" #include "commands/detailscommand.h" #include "commands/learncardkeyscommand.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::SmartCard; using namespace Kleo::Commands; NetKeyWidget::NetKeyWidget(QWidget *parent) : QWidget(parent) , mSerialNumberLabel(new QLabel(this)) , mVersionLabel(new QLabel(this)) , mLearnKeysLabel(new QLabel(this)) , mErrorLabel(new QLabel(this)) , mNullPinWidget(new NullPinWidget(this)) , mLearnKeysBtn(new QPushButton(this)) , mChangeNKSPINBtn(new QPushButton(this)) , mChangeSigGPINBtn(new QPushButton(this)) , mTreeView(new KeyTreeView(this)) , mArea(new QScrollArea) { auto vLay = new QVBoxLayout; // Set up the scroll are mArea->setFrameShape(QFrame::NoFrame); mArea->setWidgetResizable(true); auto mAreaWidget = new QWidget; mAreaWidget->setLayout(vLay); mArea->setWidget(mAreaWidget); auto scrollLay = new QVBoxLayout(this); scrollLay->setContentsMargins(0, 0, 0, 0); scrollLay->addWidget(mArea); // Add general widgets mVersionLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); vLay->addWidget(mVersionLabel, 0, Qt::AlignLeft); mSerialNumberLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); auto hLay1 = new QHBoxLayout; hLay1->addWidget(new QLabel(i18n("Serial number:"))); hLay1->addWidget(mSerialNumberLabel); hLay1->addStretch(1); vLay->addLayout(hLay1); vLay->addWidget(mNullPinWidget); auto line1 = new QFrame(); line1->setFrameShape(QFrame::HLine); vLay->addWidget(line1); vLay->addWidget(new QLabel(QStringLiteral("%1").arg(i18n("Certificates:"))), 0, Qt::AlignLeft); mLearnKeysLabel = new QLabel(i18n("There are unknown certificates on this card.")); mLearnKeysBtn->setText(i18nc("@action", "Load Certificates")); connect(mLearnKeysBtn, &QPushButton::clicked, this, [this]() { mLearnKeysBtn->setEnabled(false); auto cmd = new LearnCardKeysCommand(GpgME::CMS); cmd->setParentWidget(this); cmd->start(); auto icon = KleopatraApplication::instance()->sysTrayIcon(); if (icon) { icon->setLearningInProgress(true); } connect(cmd, &Command::finished, this, [icon]() { ReaderStatus::mutableInstance()->updateStatus(); icon->setLearningInProgress(false); }); }); auto hLay2 = new QHBoxLayout; hLay2->addWidget(mLearnKeysLabel); hLay2->addWidget(mLearnKeysBtn); hLay2->addStretch(1); vLay->addLayout(hLay2); mErrorLabel->setVisible(false); vLay->addWidget(mErrorLabel); // The certificate view mTreeView->setHierarchicalModel(AbstractKeyListModel::createHierarchicalKeyListModel(mTreeView)); mTreeView->setHierarchicalView(true); connect(mTreeView->view(), &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &idx) { const auto klm = dynamic_cast(mTreeView->view()->model()); if (!klm) { qCDebug(KLEOPATRA_LOG) << "Unhandled Model: " << mTreeView->view()->model()->metaObject()->className(); return; } auto cmd = new DetailsCommand(klm->key(idx)); cmd->setParentWidget(this); cmd->start(); }); vLay->addWidget(mTreeView); // The action area auto line2 = new QFrame(); line2->setFrameShape(QFrame::HLine); vLay->addWidget(line2); vLay->addWidget(new QLabel(QStringLiteral("%1").arg(i18n("Actions:"))), 0, Qt::AlignLeft); auto actionLayout = new QHBoxLayout(); if (CreateOpenPGPKeyFromCardKeysCommand::isSupported()) { mKeyForCardKeysButton = new QPushButton(this); mKeyForCardKeysButton->setText(i18nc("@action:button", "Create OpenPGP Key")); mKeyForCardKeysButton->setToolTip(i18nc("@info:tooltip", "Create an OpenPGP key for the keys stored on the card.")); actionLayout->addWidget(mKeyForCardKeysButton); connect(mKeyForCardKeysButton, &QPushButton::clicked, this, &NetKeyWidget::createKeyFromCardKeys); } if (!(engineInfo(GpgME::GpgSMEngine).engineVersion() < "2.2.26")) { // see https://dev.gnupg.org/T5184 mCreateCSRButton = new QPushButton(this); mCreateCSRButton->setText(i18nc("@action:button", "Create CSR")); mCreateCSRButton->setToolTip(i18nc("@info:tooltip", "Create a certificate signing request for a key stored on the card.")); mCreateCSRButton->setEnabled(false); actionLayout->addWidget(mCreateCSRButton); connect(mCreateCSRButton, &QPushButton::clicked, this, [this]() { createCSR(); }); } mChangeNKSPINBtn->setText(i18nc("NKS is an identifier for a type of keys on a NetKey card", "Change NKS PIN")); mChangeSigGPINBtn->setText(i18nc("SigG is an identifier for a type of keys on a NetKey card", "Change SigG PIN")); connect(mChangeNKSPINBtn, &QPushButton::clicked, this, [this]() { doChangePin(NetKeyCard::nksPinKeyRef()); }); connect(mChangeSigGPINBtn, &QPushButton::clicked, this, [this]() { doChangePin(NetKeyCard::sigGPinKeyRef()); }); actionLayout->addWidget(mChangeNKSPINBtn); actionLayout->addWidget(mChangeSigGPINBtn); actionLayout->addStretch(1); vLay->addLayout(actionLayout); vLay->addStretch(1); const KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("NetKeyCardView")); mTreeView->restoreLayout(configGroup); } NetKeyWidget::~NetKeyWidget() { KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("NetKeyCardView")); mTreeView->saveLayout(configGroup); } namespace { std::vector getKeysSuitableForCSRCreation(const NetKeyCard *netKeyCard) { if (netKeyCard->hasNKSNullPin()) { return {}; } std::vector keys; Kleo::copy_if(netKeyCard->keyInfos(), std::back_inserter(keys), [](const auto &keyInfo) { if (keyInfo.keyRef.substr(0, 9) == "NKS-SIGG.") { // SigG certificates for qualified signatures are issued with the physical cards; // it's not possible to request a certificate for them return false; } return keyInfo.canSign() // && (keyInfo.keyRef.substr(0, 9) == "NKS-NKS3.") // && DeVSCompliance::algorithmIsCompliant(keyInfo.algorithm); }); return keys; } } void NetKeyWidget::setCard(const NetKeyCard *card) { mSerialNumber = card->serialNumber(); mVersionLabel->setText(i18nc("1 is a Version number", "NetKey v%1 Card", card->appVersion())); mSerialNumberLabel->setText(card->displaySerialNumber()); mNullPinWidget->setSerialNumber(mSerialNumber); /* According to users of NetKey Cards it is fairly uncommon * to use SigG Certificates at all. So it should be optional to set the pins. */ mNullPinWidget->setVisible(card->hasNKSNullPin() /*|| card->hasSigGNullPin()*/); mNullPinWidget->setSigGVisible(false /*card->hasSigGNullPin()*/); mNullPinWidget->setNKSVisible(card->hasNKSNullPin()); mChangeNKSPINBtn->setEnabled(!card->hasNKSNullPin()); if (card->hasSigGNullPin()) { mChangeSigGPINBtn->setText(i18nc("SigG is an identifier for a type of keys on a NetKey card", "Set SigG PIN")); } else { mChangeSigGPINBtn->setText(i18nc("SigG is an identifier for a type of keys on a NetKey card", "Change SigG PIN")); } + const auto keys = card->keys(); mLearnKeysBtn->setEnabled(true); mLearnKeysBtn->setVisible(card->canLearnKeys()); - mTreeView->setVisible(!card->canLearnKeys()); - mLearnKeysLabel->setVisible(card->canLearnKeys()); + mTreeView->setVisible(!keys.empty()); + mLearnKeysLabel->setVisible(keys.empty()); const auto errMsg = card->errorMsg(); if (!errMsg.isEmpty()) { mErrorLabel->setText(QStringLiteral("%1: %2").arg(i18n("Error"), errMsg)); mErrorLabel->setVisible(true); } else { mErrorLabel->setVisible(false); } - const auto keys = card->keys(); mTreeView->setKeys(keys); if (mKeyForCardKeysButton) { mKeyForCardKeysButton->setEnabled(!card->hasNKSNullPin() && card->hasSigningKey() && card->hasEncryptionKey() && DeVSCompliance::algorithmIsCompliant(card->keyInfo(card->signingKeyRef()).algorithm) && DeVSCompliance::algorithmIsCompliant(card->keyInfo(card->encryptionKeyRef()).algorithm)); } if (mCreateCSRButton) { mCreateCSRButton->setEnabled(!getKeysSuitableForCSRCreation(card).empty()); } } void NetKeyWidget::doChangePin(const std::string &keyRef) { const auto netKeyCard = ReaderStatus::instance()->getCard(mSerialNumber); if (!netKeyCard) { KMessageBox::error(this, i18n("Failed to find the smartcard with the serial number: %1", QString::fromStdString(mSerialNumber))); return; } auto cmd = new ChangePinCommand(mSerialNumber, NetKeyCard::AppName, this); this->setEnabled(false); connect(cmd, &ChangePinCommand::finished, this, [this]() { this->setEnabled(true); }); cmd->setKeyRef(keyRef); if ((keyRef == NetKeyCard::nksPinKeyRef() && netKeyCard->hasNKSNullPin()) // || (keyRef == NetKeyCard::sigGPinKeyRef() && netKeyCard->hasSigGNullPin())) { cmd->setMode(ChangePinCommand::NullPinMode); } cmd->start(); } void NetKeyWidget::createKeyFromCardKeys() { auto cmd = new CreateOpenPGPKeyFromCardKeysCommand(mSerialNumber, NetKeyCard::AppName, this); this->setEnabled(false); connect(cmd, &CreateOpenPGPKeyFromCardKeysCommand::finished, this, [this]() { this->setEnabled(true); }); cmd->start(); } namespace { std::string getKeyRef(const std::vector &keys, QWidget *parent) { QStringList options; for (const auto &key : keys) { options << QStringLiteral("%1 - %2").arg(QString::fromStdString(key.keyRef), QString::fromStdString(key.grip)); } bool ok; const QString choice = QInputDialog::getItem(parent, i18n("Select Key"), i18n("Please select the key you want to create a certificate signing request for:"), options, /* current= */ 0, /* editable= */ false, &ok); return ok ? keys[options.indexOf(choice)].keyRef : std::string(); } } void NetKeyWidget::createCSR() { const auto netKeyCard = ReaderStatus::instance()->getCard(mSerialNumber); if (!netKeyCard) { KMessageBox::error(this, i18n("Failed to find the smartcard with the serial number: %1", QString::fromStdString(mSerialNumber))); return; } const auto suitableKeys = getKeysSuitableForCSRCreation(netKeyCard.get()); if (suitableKeys.empty()) { KMessageBox::error(this, i18n("Sorry! No keys suitable for creating a certificate signing request found on the smartcard.")); return; } const auto keyRef = getKeyRef(suitableKeys, this); if (keyRef.empty()) { return; } auto cmd = new CreateCSRForCardKeyCommand(keyRef, mSerialNumber, NetKeyCard::AppName, this); this->setEnabled(false); connect(cmd, &CreateCSRForCardKeyCommand::finished, this, [this]() { this->setEnabled(true); }); cmd->start(); } #include "moc_netkeywidget.cpp"