diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dae81c7c..b3168e58e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,285 +1,285 @@ # SPDX-FileCopyrightText: none # SPDX-License-Identifier: BSD-3-Clause cmake_minimum_required(VERSION 3.16 FATAL_ERROR) set(RELEASE_SERVICE_VERSION_MAJOR "23") set(RELEASE_SERVICE_VERSION_MINOR "07") set(RELEASE_SERVICE_VERSION_MICRO "70") # 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 "26") 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.105.0") set(KIDENTITYMANAGEMENT_VERSION "5.23.40") set(KMAILTRANSPORT_VERSION "5.23.40") set(AKONADI_MIME_VERSION "5.23.41") set(KMIME_VERSION "5.23.40") -set(LIBKLEO_VERSION "5.23.43") +set(LIBKLEO_VERSION "5.23.44") set(QT_REQUIRED_VERSION "5.15.2") if (QT_MAJOR_VERSION STREQUAL "6") set(QT_REQUIRED_VERSION "6.4.0") endif() set(GPGME_REQUIRED_VERSION "1.16.0") set(LIBASSUAN_REQUIRED_VERSION "2.4.2") set(GPG_ERROR_REQUIRED_VERSION "1.36") if (WIN32) set(KF5_WANT_VERSION "5.70.0") set(KMIME_WANT_VERSION "5.12.0") else () set(KF5_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 ${KF5_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) if (QT_MAJOR_VERSION STREQUAL "6") set(QT_REQUIRED_VERSION "6.4.0") set(KF_MIN_VERSION "5.240.0") set(KF_MAJOR_VERSION "6") else() set(KF_MIN_VERSION "5.105.0") set(KF_MAJOR_VERSION "5") endif() # Find KF5 packages find_package(KF${KF_MAJOR_VERSION} ${KF5_WANT_VERSION} REQUIRED COMPONENTS Codecs Config ConfigWidgets CoreAddons Crash I18n IconThemes ItemModels KCMUtils KIO WidgetsAddons WindowSystem XmlGui OPTIONAL_COMPONENTS DocTools ) set_package_properties(KF${KF_MAJOR_VERSION}DocTools 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(KF${KF_MAJOR_VERSION}DBusAddons ${KF5_WANT_VERSION} CONFIG) set_package_properties(KF${KF_MAJOR_VERSION}DBusAddons PROPERTIES DESCRIPTION "Support library to work with DBus" PURPOSE "DBus session integration" URL "https://inqlude.org/libraries/kdbusaddons.html" TYPE OPTIONAL) else() find_package(KF${KF_MAJOR_VERSION}DBusAddons ${KF5_WANT_VERSION} CONFIG REQUIRED) set(_kleopatra_dbusaddons_libs KF${KF_MAJOR_VERSION}::DBusAddons) endif() set(HAVE_QDBUS ${Qt${QT_MAJOR_VERSION}DBus_FOUND}) find_package(Gpgmepp ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) if (Gpgmepp_VERSION VERSION_GREATER_EQUAL "1.19.0") set(GPGMEPP_SUPPORTS_SET_CURVE 1) endif() if (Gpgmepp_VERSION VERSION_GREATER_EQUAL "1.19.1") set(GPGMEPP_KEY_CANSIGN_IS_FIXED 1) endif() if (QT_MAJOR_VERSION STREQUAL "6") find_package(QGpgmeQt6 ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) else() find_package(QGpgme ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) endif() if (QGpgme_VERSION VERSION_GREATER_EQUAL "1.17.0") set(QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY 1) set(QGPGME_CRYPTOCONFIGENTRY_HAS_DEFAULT_VALUE 1) set(QGPGME_SUPPORTS_WKDLOOKUP 1) set(QGPGME_SUPPORTS_IMPORT_WITH_FILTER 1) set(QGPGME_SUPPORTS_IMPORT_WITH_KEY_ORIGIN 1) set(QGPGME_SUPPORTS_SECRET_KEY_EXPORT 1) set(QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT 1) set(QGPGME_SUPPORTS_RECEIVING_KEYS_BY_KEY_ID 1) endif() if (QGpgme_VERSION VERSION_GREATER_EQUAL "1.18.0") set(QGPGME_SUPPORTS_KEY_REVOCATION 1) set(QGPGME_SUPPORTS_KEY_REFRESH 1) set(QGPGME_SUPPORTS_SET_FILENAME 1) set(QGPGME_SUPPORTS_SET_PRIMARY_UID 1) endif() if (QGpgme_VERSION VERSION_GREATER_EQUAL "1.19.0") set(QGPGME_SUPPORTS_DEFERRED_IMPORT_JOB 1) set(QGPGME_SUPPORTS_ARCHIVE_JOBS 1) set(QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS 1) endif() if (QT_MAJOR_VERSION STREQUAL "6") find_package(Qt6Core5Compat) endif() # Kdepimlibs packages find_package(KPim${KF_MAJOR_VERSION}Libkleo ${LIBKLEO_VERSION} CONFIG REQUIRED) find_package(KPim${KF_MAJOR_VERSION}Mime ${KMIME_WANT_VERSION} CONFIG REQUIRED) find_package(KPim${KF_MAJOR_VERSION}IdentityManagement ${KIDENTITYMANAGEMENT_VERSION} CONFIG) find_package(KPim${KF_MAJOR_VERSION}MailTransport ${KMAILTRANSPORT_VERSION} CONFIG) find_package(KPim${KF_MAJOR_VERSION}AkonadiMime ${AKONADI_MIME_VERSION} CONFIG) find_package(Qt${QT_MAJOR_VERSION} ${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.cmake ${CMAKE_CURRENT_BINARY_DIR}/version-kleopatra.h) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-kleopatra.h.cmake ${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 ecm_set_disabled_deprecation_versions(QT 5.10.0 KF 5.103.0) else () ecm_set_disabled_deprecation_versions(QT 6.4 KF 5.103.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(KF${KF_MAJOR_VERSION}DocTools_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}) diff --git a/src/commands/adduseridcommand.cpp b/src/commands/adduseridcommand.cpp index 68086a3bf..6ecd77a5c 100644 --- a/src/commands/adduseridcommand.cpp +++ b/src/commands/adduseridcommand.cpp @@ -1,219 +1,219 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/adduseridcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 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 "adduseridcommand.h" #include "command_p.h" #include "dialogs/adduseriddialog.h" #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; class AddUserIDCommand::Private : public Command::Private { friend class ::Kleo::Commands::AddUserIDCommand; AddUserIDCommand *q_func() const { return static_cast(q); } public: explicit Private(AddUserIDCommand *qq, KeyListController *c); ~Private() override; private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void ensureDialogCreated(); void createJob(); void showErrorDialog(const Error &error); void showSuccessDialog(); private: GpgME::Key key; QObjectCleanupHandler cleaner; QPointer dialog; QPointer job; }; AddUserIDCommand::Private *AddUserIDCommand::d_func() { return static_cast(d.get()); } const AddUserIDCommand::Private *AddUserIDCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() AddUserIDCommand::Private::Private(AddUserIDCommand *qq, KeyListController *c) : Command::Private{qq, c} { } AddUserIDCommand::Private::~Private() = default; AddUserIDCommand::AddUserIDCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{this, c}} { } AddUserIDCommand::AddUserIDCommand(const GpgME::Key &key) : Command{key, new Private{this, nullptr}} { } AddUserIDCommand::~AddUserIDCommand() { qCDebug(KLEOPATRA_LOG).nospace() << this << "::" << __func__; } void AddUserIDCommand::doStart() { const std::vector keys = d->keys(); if (keys.size() != 1) { d->finished(); return; } d->key = keys.front(); if (d->key.protocol() != GpgME::OpenPGP || !d->key.hasSecret()) { d->finished(); return; } d->ensureDialogCreated(); const UserID uid = d->key.userID(0); d->dialog->setName(QString::fromUtf8(uid.name())); d->dialog->setEmail(Formatting::prettyEMail(uid.email(), uid.id())); d->dialog->show(); } void AddUserIDCommand::Private::slotDialogAccepted() { Q_ASSERT(dialog); createJob(); if (!job) { finished(); return; } job->startAddUid(key, dialog->userID()); } void AddUserIDCommand::Private::slotDialogRejected() { Q_EMIT q->canceled(); finished(); } void AddUserIDCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) { } else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void AddUserIDCommand::doCancel() { qCDebug(KLEOPATRA_LOG).nospace() << this << "::" << __func__; if (d->job) { d->job->slotCancel(); } } void AddUserIDCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new AddUserIDDialog; cleaner.add(dialog); applyWindowID(dialog); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void AddUserIDCommand::Private::createJob() { Q_ASSERT(!job); const auto backend = QGpgME::openpgp(); if (!backend) { return; } const auto j = backend->quickJob(); if (!j) { return; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif connect(j, &QGpgME::QuickJob::result, q, [this](const GpgME::Error &err) { slotResult(err); }); job = j; } void AddUserIDCommand::Private::showErrorDialog(const Error &err) { error(xi18nc("@info", "An error occurred while trying to add the user ID: " "%1", - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), i18nc("@title:window", "Add User ID Error")); } void AddUserIDCommand::Private::showSuccessDialog() { information(i18nc("@info", "User ID successfully added."), i18nc("@title:window", "Add User ID Succeeded")); } #undef d #undef q #include "moc_adduseridcommand.cpp" diff --git a/src/commands/authenticatepivcardapplicationcommand.cpp b/src/commands/authenticatepivcardapplicationcommand.cpp index 73369fd77..baeba50e6 100644 --- a/src/commands/authenticatepivcardapplicationcommand.cpp +++ b/src/commands/authenticatepivcardapplicationcommand.cpp @@ -1,193 +1,195 @@ /* commands/authenticatepivcardapplicationcommand.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 "authenticatepivcardapplicationcommand.h" #include "cardcommand_p.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "dialogs/pivcardapplicationadministrationkeyinputdialog.h" +#include + #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_BAD_AUTH #endif #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace Kleo::SmartCard; using namespace GpgME; class AuthenticatePIVCardApplicationCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::AuthenticatePIVCardApplicationCommand; AuthenticatePIVCardApplicationCommand *q_func() const { return static_cast(q); } public: explicit Private(AuthenticatePIVCardApplicationCommand *qq, const std::string &serialNumber, QWidget *p); ~Private() override; void init(); private: void slotResult(const Error &err); void slotDialogAccepted(); void slotDialogRejected(); private: void authenticate(const QByteArray& adminKey); void retryAskingForKey(); void ensureDialogCreated(); private: QString prompt; QPointer dialog; }; AuthenticatePIVCardApplicationCommand::Private *AuthenticatePIVCardApplicationCommand::d_func() { return static_cast(d.get()); } const AuthenticatePIVCardApplicationCommand::Private *AuthenticatePIVCardApplicationCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() AuthenticatePIVCardApplicationCommand::Private::Private(AuthenticatePIVCardApplicationCommand *qq, const std::string &serialNumber, QWidget *p) : CardCommand::Private(qq, serialNumber, p) , dialog() { } AuthenticatePIVCardApplicationCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG) << "AuthenticatePIVCardApplicationCommand::Private::~Private()"; } AuthenticatePIVCardApplicationCommand::AuthenticatePIVCardApplicationCommand(const std::string &serialNumber, QWidget *p) : CardCommand(new Private(this, serialNumber, p)) { d->init(); } void AuthenticatePIVCardApplicationCommand::Private::init() { } AuthenticatePIVCardApplicationCommand::~AuthenticatePIVCardApplicationCommand() { qCDebug(KLEOPATRA_LOG) << "AuthenticatePIVCardApplicationCommand::~AuthenticatePIVCardApplicationCommand()"; } void AuthenticatePIVCardApplicationCommand::setPrompt(const QString& prompt) { d->prompt = prompt; } void AuthenticatePIVCardApplicationCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "AuthenticatePIVCardApplicationCommand::doStart()"; // at first, try to authenticate using the default application administration key d->authenticate(QByteArray::fromHex("010203040506070801020304050607080102030405060708")); } void AuthenticatePIVCardApplicationCommand::doCancel() { } void AuthenticatePIVCardApplicationCommand::Private::authenticate(const QByteArray& adminKey) { qCDebug(KLEOPATRA_LOG) << "AuthenticatePIVCardApplicationCommand::authenticate()"; const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const QByteArray plusPercentEncodedAdminKey = adminKey.toPercentEncoding().replace(' ', '+'); const QByteArray command = QByteArray("SCD SETATTR AUTH-ADM-KEY ") + plusPercentEncodedAdminKey; ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, command, q, [this](const GpgME::Error &err) { slotResult(err); }); } void AuthenticatePIVCardApplicationCommand::Private::slotResult(const Error &err) { qCDebug(KLEOPATRA_LOG) << "AuthenticatePIVCardApplicationCommand::slotResult():" - << err.asString() << "(" << err.code() << ")"; + << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err.isCanceled()) { canceled(); return; } if (err) { #ifdef GPG_ERROR_HAS_BAD_AUTH if (err.code() == GPG_ERR_BAD_AUTH) { retryAskingForKey(); return; } #endif - error(i18nc("@info", "Authenticating to the card failed: %1", QString::fromLatin1(err.asString()))); + error(i18nc("@info", "Authenticating to the card failed: %1", Formatting::errorAsString(err))); } finished(); } void AuthenticatePIVCardApplicationCommand::Private::retryAskingForKey() { ensureDialogCreated(); Q_ASSERT(dialog); dialog->show(); } void AuthenticatePIVCardApplicationCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new PIVCardApplicationAdministrationKeyInputDialog(parentWidgetOrView()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setLabelText(prompt.isEmpty() ? i18n("Please enter the PIV Card Application Administration Key in hex-encoded form.") : prompt); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void AuthenticatePIVCardApplicationCommand::Private::slotDialogAccepted() { authenticate(dialog->adminKey()); } void AuthenticatePIVCardApplicationCommand::Private::slotDialogRejected() { canceled(); } #undef d #undef q #include "moc_authenticatepivcardapplicationcommand.cpp" diff --git a/src/commands/certificatetopivcardcommand.cpp b/src/commands/certificatetopivcardcommand.cpp index 02c183dd4..da914f610 100644 --- a/src/commands/certificatetopivcardcommand.cpp +++ b/src/commands/certificatetopivcardcommand.cpp @@ -1,265 +1,265 @@ /* commands/certificatetopivcardcommand.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 #include "certificatetopivcardcommand.h" #include "cardcommand_p.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "utils/writecertassuantransaction.h" #include #include #include #include #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class CertificateToPIVCardCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::CertificateToPIVCardCommand; CertificateToPIVCardCommand *q_func() const { return static_cast(q); } public: explicit Private(CertificateToPIVCardCommand *qq, const std::string &slot, const std::string &serialno); ~Private() override; private: void start(); void startCertificateToPIVCard(); void authenticate(); void authenticationFinished(); void authenticationCanceled(); private: std::string cardSlot; Key certificate; bool hasBeenCanceled = false; }; CertificateToPIVCardCommand::Private *CertificateToPIVCardCommand::d_func() { return static_cast(d.get()); } const CertificateToPIVCardCommand::Private *CertificateToPIVCardCommand::d_func() const { return static_cast(d.get()); } #define q q_func() #define d d_func() CertificateToPIVCardCommand::Private::Private(CertificateToPIVCardCommand *qq, const std::string &slot, const std::string &serialno) : CardCommand::Private(qq, serialno, nullptr) , cardSlot(slot) { } CertificateToPIVCardCommand::Private::~Private() { } namespace { static Key getCertificateToWriteToPIVCard(const std::string &cardSlot, const std::shared_ptr &card) { if (!cardSlot.empty()) { const std::string cardKeygrip = card->keyInfo(cardSlot).grip; const auto certificate = KeyCache::instance()->findSubkeyByKeyGrip(cardKeygrip).parent(); if (certificate.isNull() || certificate.protocol() != GpgME::CMS) { return Key(); } if ((cardSlot == PIVCard::pivAuthenticationKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::cardAuthenticationKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::digitalSignatureKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::keyManagementKeyRef() && certificate.canEncrypt())) { return certificate; } } return Key(); } } void CertificateToPIVCardCommand::Private::start() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::Private::start()"; const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } certificate = getCertificateToWriteToPIVCard(cardSlot, pivCard); if (certificate.isNull()) { error(i18n("Sorry! No suitable certificate to write to this card slot was found.")); finished(); return; } const QString certificateInfo = i18nc("X.509 certificate DN (validity, created: date)", "%1 (%2, created: %3)", DN(certificate.userID(0).id()).prettyDN(), Formatting::complianceStringShort(certificate), Formatting::creationDateString(certificate)); const QString message = i18nc( "@info %1 name of card slot, %2 serial number of card", "

Please confirm that you want to write the following certificate to the %1 slot of card %2:

" "
%3
", PIVCard::keyDisplayName(cardSlot), QString::fromStdString(serialNumber()), certificateInfo); auto confirmButton = KStandardGuiItem::ok(); confirmButton.setText(i18nc("@action:button", "Write certificate")); confirmButton.setToolTip(QString()); const auto choice = KMessageBox::questionTwoActions( parentWidgetOrView(), message, i18nc("@title:window", "Write certificate to card"), confirmButton, KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::WindowModal); if (choice != KMessageBox::ButtonCode::PrimaryAction) { finished(); return; } startCertificateToPIVCard(); } void CertificateToPIVCardCommand::Private::startCertificateToPIVCard() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::Private::startCertificateToPIVCard()"; auto ctx = Context::createForProtocol(GpgME::CMS); QGpgME::QByteArrayDataProvider dp; Data data(&dp); const Error err = ctx->exportPublicKeys(certificate.primaryFingerprint(), data); if (err) { - error(i18nc("@info", "Exporting the certificate failed: %1", QString::fromUtf8(err.asString()))); + error(i18nc("@info", "Exporting the certificate failed: %1", Formatting::errorAsString(err))); finished(); return; } const QByteArray certificateData = dp.data(); const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const QByteArray command = QByteArrayLiteral("SCD WRITECERT ") + QByteArray::fromStdString(cardSlot); auto transaction = std::unique_ptr(new WriteCertAssuanTransaction(certificateData)); ReaderStatus::mutableInstance()->startTransaction( pivCard, command, q_func(), [this](const GpgME::Error &err) { q->certificateToPIVCardDone(err); }, std::move(transaction)); } void CertificateToPIVCardCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void CertificateToPIVCardCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticationFinished()"; if (!hasBeenCanceled) { startCertificateToPIVCard(); } } void CertificateToPIVCardCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } CertificateToPIVCardCommand::CertificateToPIVCardCommand(const std::string& cardSlot, const std::string &serialno) : CardCommand(new Private(this, cardSlot, serialno)) { } CertificateToPIVCardCommand::~CertificateToPIVCardCommand() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::~CertificateToPIVCardCommand()"; } void CertificateToPIVCardCommand::certificateToPIVCardDone(const Error &err) { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::certificateToPIVCardDone():" - << err.asString() << "(" << err.code() << ")"; + << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err) { #ifdef GPG_ERROR_HAS_NO_AUTH // gpgme 1.13 reports "BAD PIN" instead of "NO AUTH" if (err.code() == GPG_ERR_NO_AUTH || err.code() == GPG_ERR_BAD_PIN) { d->authenticate(); return; } #endif - d->error(i18nc("@info", "Writing the certificate to the card failed: %1", QString::fromUtf8(err.asString()))); + d->error(i18nc("@info", "Writing the certificate to the card failed: %1", Formatting::errorAsString(err))); } else if (!err.isCanceled()) { d->success(i18nc("@info", "Writing the certificate to the card succeeded.")); ReaderStatus::mutableInstance()->updateStatus(); } d->finished(); } void CertificateToPIVCardCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::doStart()"; d->start(); } void CertificateToPIVCardCommand::doCancel() { } #undef q_func #undef d_func diff --git a/src/commands/certifycertificatecommand.cpp b/src/commands/certifycertificatecommand.cpp index 0af9f0fb7..30ddb98a0 100644 --- a/src/commands/certifycertificatecommand.cpp +++ b/src/commands/certifycertificatecommand.cpp @@ -1,346 +1,346 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/signcertificatecommand.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 "exportopenpgpcertstoservercommand.h" #include "dialogs/certifycertificatedialog.h" #include "utils/keys.h" #include "utils/tags.h" #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 secKey.canCertify() && 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?"), i18n("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); if (!(d->target.keyListMode() & GpgME::SignatureNotations)) { d->target.update(); } d->dialog->setCertificateToCertify(d->target); if (d->uids.size()) { d->dialog->setSelectedUserIDs(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), - QString::fromUtf8(err.asString())), + 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->setNonRevocable(dialog->nonRevocableCertificationSelected()); 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; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif 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/changeexpirycommand.cpp b/src/commands/changeexpirycommand.cpp index c9f47c499..00d24a31f 100644 --- a/src/commands/changeexpirycommand.cpp +++ b/src/commands/changeexpirycommand.cpp @@ -1,292 +1,292 @@ /* -*- 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 #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 { #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY bool allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(const Key &key) { Q_ASSERT(!key.isNull() && key.numSubkeys() > 0); const auto subkeys = key.subkeys(); const auto primaryKey = subkeys[0]; if (primaryKey.neverExpires()) { 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 return subkey.isRevoked() || subkey.neverExpires(); }); } const auto primaryExpiration = quint32(primaryKey.expirationTime()); const auto range = std::make_pair(primaryExpiration > 10 ? primaryExpiration - 10 : 0, primaryExpiration < std::numeric_limits::max() - 10 ? primaryExpiration + 10 : std::numeric_limits::max()); return std::all_of(std::begin(subkeys), std::end(subkeys), [range](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() || (range.first <= quint32(subkey.expirationTime()) && quint32(subkey.expirationTime()) <= range.second); }); } #endif } 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, 59}; const QDateTime expiry{dialog->dateOfExpiry(), END_OF_DAY}; qCDebug(KLEOPATRA_LOG) << "expiry" << expiry; createJob(); Q_ASSERT(job); #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY if (subkey.isNull() && dialog->updateExpirationOfAllSubkeys()) { job->setOptions(ChangeExpiryJob::UpdateAllSubkeys); } #endif std::vector subkeys; if (!subkey.isNull() && subkey.keyID() != key.keyID()) { // ignore the primary subkey subkeys.push_back(subkey); } if (const Error err = job->start(key, expiry, subkeys)) { 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; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif 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), - QString::fromLocal8Bit(err.asString()))); + 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() : QDateTime::fromSecsSinceEpoch(quint32(subkey.expirationTime())).date()); #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY if (mode == ExpiryDialog::Mode::UpdateCertificateWithSubkeys) { d->dialog->setUpdateExpirationOfAllSubkeys(allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(d->key)); } #endif 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/changeownertrustcommand.cpp b/src/commands/changeownertrustcommand.cpp index 96b8763a5..677570155 100644 --- a/src/commands/changeownertrustcommand.cpp +++ b/src/commands/changeownertrustcommand.cpp @@ -1,281 +1,281 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/changeownertrustcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 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 "changeownertrustcommand.h" #include "command_p.h" #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; using namespace QGpgME; class ChangeOwnerTrustCommand::Private : public Command::Private { friend class ::Kleo::Commands::ChangeOwnerTrustCommand; ChangeOwnerTrustCommand *q_func() const { return static_cast(q); } public: Private(ChangeOwnerTrustCommand *qq, KeyListController *c); private: void startJob(Key::OwnerTrust trust); void createJob(); void slotResult(const Error &err); void showErrorDialog(const Error &error); void showSuccessDialog(); private: QPointer job; Key::OwnerTrust trustToSet = Key::OwnerTrust::Unknown; }; ChangeOwnerTrustCommand::Private *ChangeOwnerTrustCommand::d_func() { return static_cast(d.get()); } const ChangeOwnerTrustCommand::Private *ChangeOwnerTrustCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ChangeOwnerTrustCommand::Private::Private(ChangeOwnerTrustCommand *qq, KeyListController *c) : Command::Private{qq, c} { } ChangeOwnerTrustCommand::ChangeOwnerTrustCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{this, c}} { } ChangeOwnerTrustCommand::~ChangeOwnerTrustCommand() { qCDebug(KLEOPATRA_LOG) << this << __func__; } void ChangeOwnerTrustCommand::doStart() { if (d->keys().size() != 1) { d->finished(); return; } const Key key = d->key(); if (key.protocol() != GpgME::OpenPGP) { d->finished(); return; } const auto keyInfo = Formatting::formatForComboBox(key); if (key.hasSecret()) { const auto answer = KMessageBox::questionTwoActionsCancel(d->parentWidgetOrView(), xi18nc("@info", "Is '%1' your own certificate?", keyInfo), i18nc("@title:window", "Mark Own Certificate"), KGuiItem(i18nc("@action:button", "Yes, it's mine")), KGuiItem(i18nc("@action:button", "No, it's not mine")), KStandardGuiItem::cancel()); switch (answer) { case KMessageBox::ButtonCode::PrimaryAction: { if (key.ownerTrust() < Key::Ultimate) { d->startJob(Key::OwnerTrust::Ultimate); } return; } case KMessageBox::ButtonCode::SecondaryAction: { if (key.ownerTrust() == Key::Ultimate) { d->startJob(Key::OwnerTrust::Unknown); return; } // else ask next question break; } case KMessageBox::Cancel: { d->canceled(); return; } default:; // cannot happen } } if (key.ownerTrust() < Key::OwnerTrust::Full) { const auto text = (DeVSCompliance::isCompliant() && DeVSCompliance::allSubkeysAreCompliant(key)) ? xi18nc("@info %1: a certificate, %2: name of a compliance mode", "Do you want to grant '%1' the power to mark certificates as %2 for you?" "This means that the owner of this certificate properly checks fingerprints " "and confirms the identities of others.", keyInfo, DeVSCompliance::name()) : xi18nc("@info %1: a certificate", "Do you want to grant '%1' the power to mark certificates as valid for you?" "This means that the owner of this certificate properly checks fingerprints " "and confirms the identities of others.", keyInfo); const auto answer = KMessageBox::questionTwoActions(d->parentWidgetOrView(), text, i18nc("@title:window", "Grant Certification Power"), KGuiItem(i18nc("@action:button", "Grant Power")), KStandardGuiItem::cancel()); if (answer == KMessageBox::ButtonCode::PrimaryAction) { d->startJob(Key::OwnerTrust::Full); } else { d->canceled(); } } else { const auto text = (DeVSCompliance::isCompliant() && DeVSCompliance::allSubkeysAreCompliant(key)) ? xi18nc("@info %1: a certificate, %2: name of a compliance mode", "The certificate '%1' is empowered to mark other certificates as %2 for you." "Do you want to revoke this power?", keyInfo, DeVSCompliance::name()) : xi18nc("@info %1: a certificate", "The certificate '%1' is empowered to mark other certificates as valid for you." "Do you want to revoke this power?", keyInfo); const auto answer = KMessageBox::questionTwoActions(d->parentWidgetOrView(), text, i18nc("@title:window", "Revoke Certification Power"), KGuiItem(i18nc("@action:button", "Revoke Power")), KStandardGuiItem::cancel()); if (answer == KMessageBox::ButtonCode::PrimaryAction) { d->startJob(Key::OwnerTrust::Unknown); } else { d->canceled(); } } } void ChangeOwnerTrustCommand::Private::startJob(Key::OwnerTrust trust) { trustToSet = trust; createJob(); Q_ASSERT(job); if (const Error err = job->start(key(), trust)) { showErrorDialog(err); finished(); } } void ChangeOwnerTrustCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) ; else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void ChangeOwnerTrustCommand::doCancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->job) { d->job->slotCancel(); } } void ChangeOwnerTrustCommand::Private::createJob() { Q_ASSERT(!job); ChangeOwnerTrustJob *const j = QGpgME::openpgp()->changeOwnerTrustJob(); if (!j) { return; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif connect(j, &ChangeOwnerTrustJob::result, q, [this](const GpgME::Error &result) { slotResult(result); }); job = j; } void ChangeOwnerTrustCommand::Private::showErrorDialog(const Error &err) { const auto keyInfo = Formatting::formatForComboBox(key()); switch (trustToSet) { case Key::OwnerTrust::Ultimate: error(xi18nc("@info", "An error occurred while marking certificate '%1' as your certificate." "%2", keyInfo, - QString::fromUtf8(err.asString()))); + Formatting::errorAsString(err))); break; case Key::OwnerTrust::Full: error(xi18nc("@info", "An error occurred while granting certification power to '%1'." "%2", keyInfo, - QString::fromUtf8(err.asString()))); + Formatting::errorAsString(err))); break; default: error(xi18nc("@info", "An error occurred while revoking the certification power of '%1'." "%2", keyInfo, - QString::fromUtf8(err.asString()))); + Formatting::errorAsString(err))); } } void ChangeOwnerTrustCommand::Private::showSuccessDialog() { auto updatedKey = key(); updatedKey.update(); KeyCache::mutableInstance()->insert(updatedKey); const auto keyInfo = Formatting::formatForComboBox(updatedKey); switch (updatedKey.ownerTrust()) { case Key::OwnerTrust::Ultimate: success(i18nc("@info", "Certificate '%1' was marked as your certificate.", keyInfo)); break; case Key::OwnerTrust::Full: success(i18nc("@info", "Certification power was granted to '%1'.", keyInfo)); break; default: success(i18nc("@info", "The certification power of '%1' was revoked.", keyInfo)); } } #undef d #undef q #include "moc_changeownertrustcommand.cpp" diff --git a/src/commands/changepassphrasecommand.cpp b/src/commands/changepassphrasecommand.cpp index e58fef554..97f0c6eb3 100644 --- a/src/commands/changepassphrasecommand.cpp +++ b/src/commands/changepassphrasecommand.cpp @@ -1,206 +1,206 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/changepassphrasecommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "changepassphrasecommand.h" #include "command_p.h" #include #include #include #include #include #include "kleopatra_debug.h" #include using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; using namespace QGpgME; class ChangePassphraseCommand::Private : public Command::Private { friend class ::Kleo::Commands::ChangePassphraseCommand; ChangePassphraseCommand *q_func() const { return static_cast(q); } public: explicit Private(ChangePassphraseCommand *qq, KeyListController *c); ~Private() override; void init(); private: void slotResult(const Error &err); private: void createJob(); void startJob(); void showErrorDialog(const Error &error); void showSuccessDialog(); private: GpgME::Key key; QPointer job; }; ChangePassphraseCommand::Private *ChangePassphraseCommand::d_func() { return static_cast(d.get()); } const ChangePassphraseCommand::Private *ChangePassphraseCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ChangePassphraseCommand::Private::Private(ChangePassphraseCommand *qq, KeyListController *c) : Command::Private(qq, c), key(), job() { } ChangePassphraseCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG); } ChangePassphraseCommand::ChangePassphraseCommand(KeyListController *c) : Command(new Private(this, c)) { d->init(); } ChangePassphraseCommand::ChangePassphraseCommand(QAbstractItemView *v, KeyListController *c) : Command(v, new Private(this, c)) { d->init(); } ChangePassphraseCommand::ChangePassphraseCommand(const GpgME::Key &key) : Command(key, new Private(this, nullptr)) { d->init(); } void ChangePassphraseCommand::Private::init() { } ChangePassphraseCommand::~ChangePassphraseCommand() { qCDebug(KLEOPATRA_LOG); } void ChangePassphraseCommand::doStart() { const std::vector keys = d->keys(); if (keys.size() != 1 || !keys.front().hasSecret()) { d->finished(); return; } d->key = keys.front(); d->createJob(); d->startJob(); } void ChangePassphraseCommand::Private::startJob() { const Error err = job ? job->start(key) : Error::fromCode(GPG_ERR_NOT_SUPPORTED) ; if (err) { showErrorDialog(err); finished(); } } void ChangePassphraseCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) ; else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void ChangePassphraseCommand::doCancel() { qCDebug(KLEOPATRA_LOG); if (d->job) { d->job->slotCancel(); } } void ChangePassphraseCommand::Private::createJob() { Q_ASSERT(!job); const auto backend = (key.protocol() == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); if (!backend) { return; } ChangePasswdJob *const j = backend->changePasswdJob(); if (!j) { return; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif connect(j, &ChangePasswdJob::result, q, [this](const GpgME::Error &result) { slotResult(result); }); job = j; } void ChangePassphraseCommand::Private::showErrorDialog(const Error &err) { error(i18n("

An error occurred while trying to change " "the passphrase for %1:

%2

", Formatting::formatForComboBox(key), - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), i18n("Passphrase Change Error")); } void ChangePassphraseCommand::Private::showSuccessDialog() { information(i18n("Passphrase changed successfully."), i18n("Passphrase Change Succeeded")); } #undef d #undef q #include "moc_changepassphrasecommand.cpp" diff --git a/src/commands/changepincommand.cpp b/src/commands/changepincommand.cpp index d93e1a788..2f16dcbea 100644 --- a/src/commands/changepincommand.cpp +++ b/src/commands/changepincommand.cpp @@ -1,221 +1,223 @@ /* commands/changepincommand.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 "changepincommand.h" #include "cardcommand_p.h" #include "smartcard/netkeycard.h" #include "smartcard/openpgpcard.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" +#include + #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class ChangePinCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::ChangePinCommand; ChangePinCommand *q_func() const { return static_cast(q); } public: explicit Private(ChangePinCommand *qq, const std::string &serialNumber, const std::string &appName, QWidget *p); ~Private() override; void init(); private: void slotResult(const Error &err); private: void changePin(); private: std::string appName; std::string keyRef; ChangePinMode mode = NormalMode; }; ChangePinCommand::Private *ChangePinCommand::d_func() { return static_cast(d.get()); } const ChangePinCommand::Private *ChangePinCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ChangePinCommand::Private::Private(ChangePinCommand *qq, const std::string &serialNumber, const std::string &appName_, QWidget *p) : CardCommand::Private(qq, serialNumber, p) , appName(appName_) { } ChangePinCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG) << "ChangePinCommand::Private::~Private()"; } ChangePinCommand::ChangePinCommand(const std::string &serialNumber, const std::string &appName, QWidget *p) : CardCommand(new Private(this, serialNumber, appName, p)) { d->init(); } void ChangePinCommand::Private::init() { } ChangePinCommand::~ChangePinCommand() { qCDebug(KLEOPATRA_LOG) << "ChangePinCommand::~ChangePinCommand()"; } void ChangePinCommand::setKeyRef(const std::string &keyRef) { d->keyRef = keyRef; } void ChangePinCommand::setMode(ChangePinMode mode) { d->mode = mode; } void ChangePinCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "ChangePinCommand::doStart()"; d->changePin(); } void ChangePinCommand::doCancel() { } void ChangePinCommand::Private::changePin() { qCDebug(KLEOPATRA_LOG) << "ChangePinCommand::changePin()"; const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the smartcard with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } QByteArrayList command; command << "SCD PASSWD"; if (mode == ResetMode) { command << "--reset"; } else if (mode == NullPinMode) { command << "--nullpin"; } command << QByteArray::fromStdString(keyRef); ReaderStatus::mutableInstance()->startSimpleTransaction(card, command.join(' '), q, [this](const GpgME::Error &err) { slotResult(err); }); } namespace { static QString errorMessage(const std::string &keyRef, ChangePinCommand::ChangePinMode mode, const QString &errorText) { // see cmd_passwd() in gpg-card.c if (keyRef == PIVCard::pukKeyRef()) { return i18nc("@info", "Changing the PUK failed: %1", errorText); } if (keyRef == OpenPGPCard::resetCodeKeyRef()) { return i18nc("@info", "Unblocking the PIN failed: %1", errorText); } if (keyRef == OpenPGPCard::adminPinKeyRef()) { return i18nc("@info", "Changing the Admin PIN failed: %1", errorText); } if (keyRef == OpenPGPCard::resetCodeKeyRef() && mode == ChangePinCommand::ResetMode) { return i18nc("@info", "Changing the Reset Code failed: %1", errorText); } if (keyRef == NetKeyCard::nksPinKeyRef()) { if (mode == ChangePinCommand::NullPinMode) { return i18nc("@info", "Setting the NKS PIN failed: %1", errorText); } else { return i18nc("@info", "Changing the NKS PIN failed: %1", errorText); } } if (keyRef == NetKeyCard::sigGPinKeyRef()) { if (mode == ChangePinCommand::NullPinMode) { return i18nc("@info", "Setting the SigG PIN failed: %1", errorText); } else { return i18nc("@info", "Changing the SigG PIN failed: %1", errorText); } } return i18nc("@info", "Changing the PIN failed: %1", errorText); } static QString successMessage(const std::string &keyRef, ChangePinCommand::ChangePinMode mode) { // see cmd_passwd() in gpg-card.c if (keyRef == PIVCard::pukKeyRef()) { return i18nc("@info", "PUK successfully changed."); } if (keyRef == OpenPGPCard::resetCodeKeyRef()) { return i18nc("@info", "Unblocked and set a new PIN successfully."); } if (keyRef == OpenPGPCard::adminPinKeyRef()) { return i18nc("@info", "Admin PIN changed successfully."); } if (keyRef == OpenPGPCard::resetCodeKeyRef() && mode == ChangePinCommand::ResetMode) { return i18nc("@info", "Reset Code changed successfully."); } if (keyRef == NetKeyCard::nksPinKeyRef()) { if (mode == ChangePinCommand::NullPinMode) { return i18nc("@info", "NKS PIN set successfully."); } else { return i18nc("@info", "NKS PIN changed successfully."); } } if (keyRef == NetKeyCard::sigGPinKeyRef()) { if (mode == ChangePinCommand::NullPinMode) { return i18nc("@info", "SigG PIN set successfully."); } else { return i18nc("@info", "SigG PIN changed successfully."); } } return i18nc("@info", "PIN changed successfully."); } } void ChangePinCommand::Private::slotResult(const GpgME::Error& err) { qCDebug(KLEOPATRA_LOG) << "ChangePinCommand::slotResult():" - << err.asString() << "(" << err.code() << ")"; + << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err) { - error(errorMessage(keyRef, mode, QString::fromLatin1(err.asString()))); + error(errorMessage(keyRef, mode, Formatting::errorAsString(err))); } else if (!err.isCanceled()) { success(successMessage(keyRef, mode)); ReaderStatus::mutableInstance()->updateStatus(); } finished(); } #undef d #undef q #include "moc_changepincommand.cpp" diff --git a/src/commands/createcsrforcardkeycommand.cpp b/src/commands/createcsrforcardkeycommand.cpp index e2891bceb..7ad6a1810 100644 --- a/src/commands/createcsrforcardkeycommand.cpp +++ b/src/commands/createcsrforcardkeycommand.cpp @@ -1,296 +1,296 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/createcsrforcardkeycommand.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 #include "createcsrforcardkeycommand.h" #include "cardcommand_p.h" #include "dialogs/createcsrforcardkeydialog.h" #include "smartcard/netkeycard.h" #include "smartcard/openpgpcard.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "utils/filedialog.h" #include "utils/keyparameters.h" #include "utils/keyusage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace Kleo::SmartCard; using namespace GpgME; using namespace QGpgME; class CreateCSRForCardKeyCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::CreateCSRForCardKeyCommand; CreateCSRForCardKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(CreateCSRForCardKeyCommand *qq, const std::string &keyRef, const std::string &serialNumber, const std::string &appName, QWidget *parent); ~Private() override; private: void start(); void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const KeyGenerationResult &result, const QByteArray &request); QUrl saveRequest(const QByteArray &request); void ensureDialogCreated(); private: std::string appName; std::string keyRef; KeyUsage keyUsage; QPointer dialog; }; CreateCSRForCardKeyCommand::Private *CreateCSRForCardKeyCommand::d_func() { return static_cast(d.get()); } const CreateCSRForCardKeyCommand::Private *CreateCSRForCardKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() CreateCSRForCardKeyCommand::Private::Private(CreateCSRForCardKeyCommand *qq, const std::string &keyRef_, const std::string &serialNumber, const std::string &appName_, QWidget *parent) : CardCommand::Private(qq, serialNumber, parent) , appName(appName_) , keyRef(keyRef_) { } CreateCSRForCardKeyCommand::Private::~Private() { } namespace { KeyUsage getKeyUsage(const KeyPairInfo &keyInfo) { // note: gpgsm does not support creating CSRs for authentication certificates KeyUsage usage; if (keyInfo.canCertify()) { usage.setCanCertify(true); } if (keyInfo.canSign()) { usage.setCanSign(true); } if (keyInfo.canEncrypt()) { usage.setCanEncrypt(true); } return usage; } } void CreateCSRForCardKeyCommand::Private::start() { if (appName != NetKeyCard::AppName && appName != OpenPGPCard::AppName && appName != PIVCard::AppName) { qCWarning(KLEOPATRA_LOG) << "CreateCSRForCardKeyCommand does not support card application" << QString::fromStdString(appName); finished(); return; } const auto card = ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the smartcard with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const KeyPairInfo &keyInfo = card->keyInfo(keyRef); keyUsage = getKeyUsage(keyInfo); ensureDialogCreated(); dialog->setWindowTitle(i18n("Certificate Details")); if (!card->cardHolder().isEmpty()) { dialog->setName(card->cardHolder()); } dialog->show(); } void CreateCSRForCardKeyCommand::Private::slotDialogAccepted() { const Error err = ReaderStatus::switchCardAndApp(serialNumber(), appName); if (err) { finished(); return; } const auto backend = smime(); if (!backend) { finished(); return; } KeyGenerationJob *const job = backend->keyGenerationJob(); if (!job) { finished(); return; } Job::context(job)->setArmor(true); connect(job, &KeyGenerationJob::result, q, [this](const GpgME::KeyGenerationResult &result, const QByteArray &pubKeyData) { slotResult(result, pubKeyData); }); KeyParameters keyParameters(KeyParameters::CMS); keyParameters.setCardKeyRef(QString::fromStdString(keyRef)); keyParameters.setKeyUsage(keyUsage); keyParameters.setDN(dialog->dn()); keyParameters.setEmail(dialog->email()); if (const Error err = job->start(keyParameters.toString())) { - error(i18nc("@info", "Creating a CSR for the card key failed:\n%1", QString::fromUtf8(err.asString()))); + error(i18nc("@info", "Creating a CSR for the card key failed:\n%1", Formatting::errorAsString(err))); finished(); } } void CreateCSRForCardKeyCommand::Private::slotDialogRejected() { canceled(); } void CreateCSRForCardKeyCommand::Private::slotResult(const KeyGenerationResult &result, const QByteArray &request) { if (result.error().isCanceled()) { // do nothing } else if (result.error()) { - error(i18nc("@info", "Creating a CSR for the card key failed:\n%1", QString::fromUtf8(result.error().asString()))); + error(i18nc("@info", "Creating a CSR for the card key failed:\n%1", Formatting::errorAsString(result.error()))); } else { const QUrl url = saveRequest(request); if (!url.isEmpty()) { information(xi18nc("@info", "Successfully wrote request to %1." "You should now send the request to the Certification Authority (CA).", url.toLocalFile()), i18nc("@title", "Request Saved")); } } finished(); } namespace { struct SaveToFileResult { QUrl url; QString errorMessage; }; SaveToFileResult saveRequestToFile(const QString &filename, const QByteArray &request, QIODevice::OpenMode mode) { QFile file(filename); if (file.open(mode)) { const auto bytesWritten = file.write(request); if (bytesWritten < request.size()) { return { QUrl(), file.errorString() }; } return { QUrl::fromLocalFile(file.fileName()), QString() }; } return { QUrl(), file.errorString() }; } } QUrl CreateCSRForCardKeyCommand::Private::saveRequest(const QByteArray &request) { const QString proposedFilename = QLatin1String("request_%1.p10").arg(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd_HHmmss"))); while (true) { const QString filePath = FileDialog::getSaveFileNameEx( parentWidgetOrView(), i18nc("@title", "Save Request"), QStringLiteral("save_csr"), proposedFilename, i18n("PKCS#10 Requests (*.p10)")); if (filePath.isEmpty()) { // user canceled the dialog return QUrl(); } const auto result = saveRequestToFile(filePath, request, QIODevice::NewOnly); if (result.url.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "Writing request to file" << filePath << "failed:" << result.errorMessage; error(xi18nc("@info", "Saving the request failed.%1", result.errorMessage), i18nc("@title", "Error Saving Request")); } else { return result.url; } } } void CreateCSRForCardKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new CreateCSRForCardKeyDialog; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } CreateCSRForCardKeyCommand::CreateCSRForCardKeyCommand(const std::string &keyRef, const std::string &serialNumber, const std::string &appName, QWidget *parent) : CardCommand(new Private(this, keyRef, serialNumber, appName, parent)) { } CreateCSRForCardKeyCommand::~CreateCSRForCardKeyCommand() { } void CreateCSRForCardKeyCommand::doStart() { d->start(); } void CreateCSRForCardKeyCommand::doCancel() { } #undef d #undef q #include "moc_createcsrforcardkeycommand.cpp" diff --git a/src/commands/createopenpgpkeyfromcardkeyscommand.cpp b/src/commands/createopenpgpkeyfromcardkeyscommand.cpp index 0bcab5b7e..dfe299038 100644 --- a/src/commands/createopenpgpkeyfromcardkeyscommand.cpp +++ b/src/commands/createopenpgpkeyfromcardkeyscommand.cpp @@ -1,218 +1,218 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/createopenpgpkeyfromcardkeyscommand.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 #include "createopenpgpkeyfromcardkeyscommand.h" #include "cardcommand_p.h" #include "dialogs/adduseriddialog.h" #include "smartcard/netkeycard.h" #include "smartcard/openpgpcard.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; using namespace QGpgME; class CreateOpenPGPKeyFromCardKeysCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::CreateOpenPGPKeyFromCardKeysCommand; CreateOpenPGPKeyFromCardKeysCommand *q_func() const { return static_cast(q); } public: explicit Private(CreateOpenPGPKeyFromCardKeysCommand *qq, const std::string &serialNumber, const std::string &appName, QWidget *parent); ~Private() override; private: void start(); void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); void ensureDialogCreated(); private: std::string appName; QPointer dialog; }; CreateOpenPGPKeyFromCardKeysCommand::Private *CreateOpenPGPKeyFromCardKeysCommand::d_func() { return static_cast(d.get()); } const CreateOpenPGPKeyFromCardKeysCommand::Private *CreateOpenPGPKeyFromCardKeysCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() CreateOpenPGPKeyFromCardKeysCommand::Private::Private(CreateOpenPGPKeyFromCardKeysCommand *qq, const std::string &serialNumber, const std::string &appName_, QWidget *parent) : CardCommand::Private(qq, serialNumber, parent) , appName(appName_) { } CreateOpenPGPKeyFromCardKeysCommand::Private::~Private() { } void CreateOpenPGPKeyFromCardKeysCommand::Private::start() { if (appName != NetKeyCard::AppName && appName != OpenPGPCard::AppName && appName != PIVCard::AppName) { qCWarning(KLEOPATRA_LOG) << "CreateOpenPGPKeyFromCardKeysCommand does not support card application" << QString::fromStdString(appName); finished(); return; } const auto card = ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the smartcard with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const auto signingKeyGrip = card->keyInfo(card->signingKeyRef()).grip; const Key signingKey = KeyCache::instance()->findSubkeyByKeyGrip(signingKeyGrip, OpenPGP).parent(); if (!signingKey.isNull()) { const QString message = i18nc("@info", "

There is already an OpenPGP key corresponding to the signing key on this card:

%1

" "

Do you still want to create an OpenPGP key for the card keys?

", Formatting::summaryLine(signingKey)); const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Create OpenPGP Key"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify); if (choice != KMessageBox::Continue) { finished(); return; } } ensureDialogCreated(); dialog->setWindowTitle(i18n("Enter User ID")); dialog->setName(card->cardHolder()); dialog->show(); } void CreateOpenPGPKeyFromCardKeysCommand::Private::slotDialogAccepted() { const Error err = ReaderStatus::switchCardAndApp(serialNumber(), appName); if (err) { finished(); return; } const auto backend = openpgp(); if (!backend) { finished(); return; } QuickJob *const job = backend->quickJob(); if (!job) { finished(); return; } connect(job, &QGpgME::QuickJob::result, q, [this](const GpgME::Error &error) { slotResult(error); }); const QString userID = Formatting::prettyNameAndEMail(OpenPGP, QString(), dialog->name(), dialog->email()); const QDateTime expires = QDateTime(); const unsigned int flags = GPGME_CREATE_FORCE; job->startCreate(userID, "card", expires, Key(), flags); } void CreateOpenPGPKeyFromCardKeysCommand::Private::slotDialogRejected() { canceled(); } void CreateOpenPGPKeyFromCardKeysCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) { // do nothing } else if (err) { - error(i18nc("@info", "Creating an OpenPGP key from the card keys failed: %1", QString::fromUtf8(err.asString()))); + error(i18nc("@info", "Creating an OpenPGP key from the card keys failed: %1", Formatting::errorAsString(err))); } else { success(i18nc("@info", "Successfully generated an OpenPGP key from the card keys.")); } finished(); } void CreateOpenPGPKeyFromCardKeysCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new AddUserIDDialog; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } CreateOpenPGPKeyFromCardKeysCommand::CreateOpenPGPKeyFromCardKeysCommand(const std::string &serialNumber, const std::string &appName, QWidget *parent) : CardCommand(new Private(this, serialNumber, appName, parent)) { } CreateOpenPGPKeyFromCardKeysCommand::~CreateOpenPGPKeyFromCardKeysCommand() { } // static bool CreateOpenPGPKeyFromCardKeysCommand::isSupported() { return !(engineInfo(GpgEngine).engineVersion() < "2.3.0"); } void CreateOpenPGPKeyFromCardKeysCommand::doStart() { d->start(); } void CreateOpenPGPKeyFromCardKeysCommand::doCancel() { } #undef d #undef q #include "moc_createopenpgpkeyfromcardkeyscommand.cpp" diff --git a/src/commands/deletecertificatescommand.cpp b/src/commands/deletecertificatescommand.cpp index 9a346bae7..56cdb7f14 100644 --- a/src/commands/deletecertificatescommand.cpp +++ b/src/commands/deletecertificatescommand.cpp @@ -1,395 +1,396 @@ /* -*- mode: c++; c-basic-offset:4 -*- deleteCertificatescommand.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 "deletecertificatescommand.h" #include "command_p.h" #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace GpgME; using namespace Kleo; using namespace Kleo::Dialogs; using namespace QGpgME; class DeleteCertificatesCommand::Private : public Command::Private { friend class ::Kleo::DeleteCertificatesCommand; DeleteCertificatesCommand *q_func() const { return static_cast(q); } public: explicit Private(DeleteCertificatesCommand *qq, KeyListController *c); ~Private() override; void startDeleteJob(GpgME::Protocol protocol); void cancelJobs(); void pgpDeleteResult(const GpgME::Error &); void cmsDeleteResult(const GpgME::Error &); void showErrorsAndFinish(); bool canDelete(GpgME::Protocol proto) const { if (const auto cbp = (proto == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) if (DeleteJob *const job = cbp->deleteJob()) { job->slotCancel(); return true; } return false; } void ensureDialogCreated() { if (dialog) { return; } dialog = new DeleteCertificatesDialog; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle(i18nc("@title:window", "Delete Certificates")); connect(dialog, &QDialog::accepted, q_func(), [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q_func(), [this]() { slotDialogRejected(); }); } void ensureDialogShown() { if (dialog) { dialog->show(); } } void slotDialogAccepted(); void slotDialogRejected() { canceled(); } private: QPointer dialog; QPointer cmsJob, pgpJob; GpgME::Error cmsError, pgpError; std::vector cmsKeys, pgpKeys; }; DeleteCertificatesCommand::Private *DeleteCertificatesCommand::d_func() { return static_cast(d.get()); } const DeleteCertificatesCommand::Private *DeleteCertificatesCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() DeleteCertificatesCommand::Private::Private(DeleteCertificatesCommand *qq, KeyListController *c) : Command::Private(qq, c) { } DeleteCertificatesCommand::Private::~Private() {} DeleteCertificatesCommand::DeleteCertificatesCommand(KeyListController *p) : Command(new Private(this, p)) { } DeleteCertificatesCommand::DeleteCertificatesCommand(QAbstractItemView *v, KeyListController *p) : Command(v, new Private(this, p)) { } DeleteCertificatesCommand::~DeleteCertificatesCommand() {} namespace { enum Action { Nothing = 0, Failure = 1, ClearCMS = 2, ClearPGP = 4 }; // const unsigned int errorCase = // openpgp.empty() << 3U | d->canDelete( OpenPGP ) << 2U | // cms.empty() << 1U | d->canDelete( CMS ) << 0U ; static const struct { const KLazyLocalizedString text; Action actions; } deletionErrorCases[16] = { // if havePGP // if cantPGP // if haveCMS {kli18n("Neither the OpenPGP nor the CMS " "backends support certificate deletion.\n" "Check your installation."), Failure}, // cantCMS {kli18n("The OpenPGP backend does not support " "certificate deletion.\n" "Check your installation.\n" "Only the selected CMS certificates " "will be deleted."), ClearPGP}, // canCMS // if !haveCMS {kli18n("The OpenPGP backend does not support " "certificate deletion.\n" "Check your installation."), Failure}, {kli18n("The OpenPGP backend does not support " "certificate deletion.\n" "Check your installation."), Failure}, // if canPGP // if haveCMS {kli18n("The CMS backend does not support " "certificate deletion.\n" "Check your installation.\n" "Only the selected OpenPGP certificates " "will be deleted."), ClearCMS}, // cantCMS {KLazyLocalizedString(), Nothing}, // canCMS // if !haveCMS {KLazyLocalizedString(), Nothing}, // cantCMS {KLazyLocalizedString(), Nothing}, // canCMS // if !havePGP // if cantPGP // if haveCMS {kli18n("The CMS backend does not support " "certificate deletion.\n" "Check your installation."), Failure}, // cantCMS {KLazyLocalizedString(), Nothing}, // canCMS // if !haveCMS {KLazyLocalizedString(), Nothing}, // cantCMS {KLazyLocalizedString(), Nothing}, // canCMS // if canPGP // if haveCMS {kli18n("The CMS backend does not support " "certificate deletion.\n" "Check your installation."), Failure}, // cantCMS {KLazyLocalizedString(), Nothing}, // canCMS // if !haveCMS {KLazyLocalizedString(), Nothing}, // cantCMS {KLazyLocalizedString(), Nothing}, // canCMS }; } // anon namespace void DeleteCertificatesCommand::doStart() { std::vector selected = d->keys(); if (selected.empty()) { d->finished(); return; } std::sort(selected.begin(), selected.end(), _detail::ByFingerprint()); // Calculate the closure of the selected keys (those that need to // be deleted with them, though not selected themselves): std::vector toBeDeleted = KeyCache::instance()->findSubjects(selected); std::sort(toBeDeleted.begin(), toBeDeleted.end(), _detail::ByFingerprint()); std::vector unselected; unselected.reserve(toBeDeleted.size()); std::set_difference(toBeDeleted.begin(), toBeDeleted.end(), selected.begin(), selected.end(), std::back_inserter(unselected), _detail::ByFingerprint()); d->ensureDialogCreated(); d->dialog->setSelectedKeys(selected); d->dialog->setUnselectedKeys(unselected); d->ensureDialogShown(); } void DeleteCertificatesCommand::Private::slotDialogAccepted() { std::vector keys = dialog->keys(); Q_ASSERT(!keys.empty()); auto pgpBegin = keys.begin(); auto pgpEnd = std::stable_partition(pgpBegin, keys.end(), [](const GpgME::Key &key) { return key.protocol() != GpgME::CMS; }); auto cmsBegin = pgpEnd; auto cmsEnd = keys.end(); std::vector openpgp(pgpBegin, pgpEnd); std::vector cms(cmsBegin, cmsEnd); const unsigned int errorCase = openpgp.empty() << 3U | canDelete(OpenPGP) << 2U | cms.empty() << 1U | canDelete(CMS) << 0U; if (const unsigned int actions = deletionErrorCases[errorCase].actions) { information(KLocalizedString(deletionErrorCases[errorCase].text).toString(), (actions & Failure) ? i18n("Certificate Deletion Failed") : i18n("Certificate Deletion Problem")); if (actions & ClearCMS) { cms.clear(); } if (actions & ClearPGP) { openpgp.clear(); } if (actions & Failure) { canceled(); return; } } Q_ASSERT(!openpgp.empty() || !cms.empty()); pgpKeys.swap(openpgp); cmsKeys.swap(cms); if (!pgpKeys.empty()) { startDeleteJob(GpgME::OpenPGP); } if (!cmsKeys.empty()) { startDeleteJob(GpgME::CMS); } if ((pgpKeys.empty() || pgpError.code()) && (cmsKeys.empty() || cmsError.code())) { showErrorsAndFinish(); } } void DeleteCertificatesCommand::Private::startDeleteJob(GpgME::Protocol protocol) { Q_ASSERT(protocol != GpgME::UnknownProtocol); const std::vector &keys = protocol == CMS ? cmsKeys : pgpKeys; const auto backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); Q_ASSERT(backend); std::unique_ptr job(new MultiDeleteJob(backend)); connect(job.get(), &QGpgME::MultiDeleteJob::result, q_func(), [this, protocol](const GpgME::Error &result) { if (protocol == CMS) { cmsDeleteResult(result); } else { pgpDeleteResult(result); } }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(job.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif if (const Error err = job->start(keys, true /*allowSecretKeyDeletion*/)) { (protocol == CMS ? cmsError : pgpError) = err; } else { (protocol == CMS ? cmsJob : pgpJob) = job.release(); } } void DeleteCertificatesCommand::Private::showErrorsAndFinish() { Q_ASSERT(!pgpJob); Q_ASSERT(!cmsJob); if (pgpError || cmsError) { QString pgpErrorString; if (pgpError) { - pgpErrorString = i18n("OpenPGP backend: %1", QString::fromLocal8Bit(pgpError.asString())); + pgpErrorString = i18n("OpenPGP backend: %1", Formatting::errorAsString(pgpError)); } QString cmsErrorString; if (cmsError) { - cmsErrorString = i18n("CMS backend: %1", QString::fromLocal8Bit(cmsError.asString())); + cmsErrorString = i18n("CMS backend: %1", Formatting::errorAsString(cmsError)); } const QString msg = i18n("

An error occurred while trying to delete " "the certificate:

" "

%1

", pgpError ? cmsError ? pgpErrorString + QLatin1String("
") + cmsErrorString : pgpErrorString : cmsErrorString); error(msg, i18n("Certificate Deletion Failed")); } else if (!pgpError.isCanceled() && !cmsError.isCanceled()) { std::vector keys = pgpKeys; keys.insert(keys.end(), cmsKeys.begin(), cmsKeys.end()); KeyCache::mutableInstance()->remove(keys); } finished(); } void DeleteCertificatesCommand::doCancel() { d->cancelJobs(); } void DeleteCertificatesCommand::Private::pgpDeleteResult(const Error &err) { pgpError = err; pgpJob = nullptr; if (!cmsJob) { showErrorsAndFinish(); } } void DeleteCertificatesCommand::Private::cmsDeleteResult(const Error &err) { cmsError = err; cmsJob = nullptr; if (!pgpJob) { showErrorsAndFinish(); } } void DeleteCertificatesCommand::Private::cancelJobs() { if (cmsJob) { cmsJob->slotCancel(); } if (pgpJob) { pgpJob->slotCancel(); } } #undef d #undef q #include "moc_deletecertificatescommand.cpp" diff --git a/src/commands/exportcertificatecommand.cpp b/src/commands/exportcertificatecommand.cpp index c8365ab09..ca6af841d 100644 --- a/src/commands/exportcertificatecommand.cpp +++ b/src/commands/exportcertificatecommand.cpp @@ -1,369 +1,369 @@ /* -*- mode: c++; c-basic-offset:4 -*- exportcertificatecommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "exportcertificatecommand.h" #include "fileoperationspreferences.h" #include "command_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; using namespace QGpgME; class ExportCertificateCommand::Private : public Command::Private { friend class ::ExportCertificateCommand; ExportCertificateCommand *q_func() const { return static_cast(q); } public: explicit Private(ExportCertificateCommand *qq, KeyListController *c); ~Private() override; void startExportJob(GpgME::Protocol protocol, const std::vector &keys); void cancelJobs(); void exportResult(const GpgME::Error &, const QByteArray &); void showError(const GpgME::Error &error); bool requestFileNames(GpgME::Protocol prot); void finishedIfLastJob(); private: QMap fileNames; uint jobsPending = 0; QMap outFileForSender; QPointer cmsJob; QPointer pgpJob; }; ExportCertificateCommand::Private *ExportCertificateCommand::d_func() { return static_cast(d.get()); } const ExportCertificateCommand::Private *ExportCertificateCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ExportCertificateCommand::Private::Private(ExportCertificateCommand *qq, KeyListController *c) : Command::Private(qq, c) { } ExportCertificateCommand::Private::~Private() {} ExportCertificateCommand::ExportCertificateCommand(KeyListController *p) : Command(new Private(this, p)) { } ExportCertificateCommand::ExportCertificateCommand(QAbstractItemView *v, KeyListController *p) : Command(v, new Private(this, p)) { } ExportCertificateCommand::ExportCertificateCommand(const Key &key) : Command(key, new Private(this, nullptr)) { } ExportCertificateCommand::~ExportCertificateCommand() {} void ExportCertificateCommand::setOpenPGPFileName(const QString &fileName) { if (!d->jobsPending) { d->fileNames[OpenPGP] = fileName; } } QString ExportCertificateCommand::openPGPFileName() const { return d->fileNames[OpenPGP]; } void ExportCertificateCommand::setX509FileName(const QString &fileName) { if (!d->jobsPending) { d->fileNames[CMS] = fileName; } } QString ExportCertificateCommand::x509FileName() const { return d->fileNames[CMS]; } void ExportCertificateCommand::doStart() { std::vector keys = d->keys(); if (keys.empty()) { return; } const auto firstCms = std::partition(keys.begin(), keys.end(), [](const GpgME::Key &key) { return key.protocol() != GpgME::CMS; }); std::vector openpgp, cms; std::copy(keys.begin(), firstCms, std::back_inserter(openpgp)); std::copy(firstCms, keys.end(), std::back_inserter(cms)); Q_ASSERT(!openpgp.empty() || !cms.empty()); const bool haveBoth = !cms.empty() && !openpgp.empty(); const GpgME::Protocol prot = haveBoth ? UnknownProtocol : (!cms.empty() ? CMS : OpenPGP); if (!d->requestFileNames(prot)) { Q_EMIT canceled(); d->finished(); } else { if (!openpgp.empty()) { d->startExportJob(GpgME::OpenPGP, openpgp); } if (!cms.empty()) { d->startExportJob(GpgME::CMS, cms); } } } bool ExportCertificateCommand::Private::requestFileNames(GpgME::Protocol protocol) { if (protocol == UnknownProtocol) { if (!fileNames[GpgME::OpenPGP].isEmpty() && !fileNames[GpgME::CMS].isEmpty()) { return true; } /* Unknown protocol ask for first PGP Export file name */ if (fileNames[GpgME::OpenPGP].isEmpty() && !requestFileNames(GpgME::OpenPGP)) { return false; } /* And then for CMS */ return requestFileNames(GpgME::CMS); } if (!fileNames[protocol].isEmpty()) { return true; } const auto lastDir = ApplicationState::lastUsedExportDirectory(); QString proposedFileName = lastDir + QLatin1Char('/'); if (keys().size() == 1) { const bool usePGPFileExt = FileOperationsPreferences().usePGPFileExt(); const auto key = keys().front(); auto name = Formatting::prettyName(key); if (name.isEmpty()) { name = Formatting::prettyEMail(key); } /* Not translated so it's better to use in tutorials etc. */ proposedFileName += QStringLiteral("%1_%2_public.%3").arg(name).arg( Formatting::prettyKeyID(key.shortKeyID())).arg( QString::fromLatin1(outputFileExtension(protocol == OpenPGP ? Class::OpenPGP | Class::Ascii | Class::Certificate : Class::CMS | Class::Ascii | Class::Certificate, usePGPFileExt))); } if (protocol == GpgME::CMS) { if (!fileNames[GpgME::OpenPGP].isEmpty()) { /* If the user has already selected a PGP file name then use that as basis * for a proposal for the S/MIME file. */ proposedFileName = fileNames[GpgME::OpenPGP]; const int idx = proposedFileName.size() - 4; if (proposedFileName.endsWith(QLatin1String(".asc"))) { proposedFileName.replace(idx, 4, QLatin1String(".pem")); } if (proposedFileName.endsWith(QLatin1String(".gpg")) || proposedFileName.endsWith(QLatin1String(".pgp"))) { proposedFileName.replace(idx, 4, QLatin1String(".der")); } } } if (proposedFileName.isEmpty()) { proposedFileName = lastDir; proposedFileName += i18nc("A generic filename for exported certificates", "certificates"); proposedFileName += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem"); } auto fname = FileDialog::getSaveFileNameEx(parentWidgetOrView(), i18nc("1 is protocol", "Export %1 Certificates", Formatting::displayName(protocol)), QStringLiteral("imp"), proposedFileName, protocol == GpgME::OpenPGP ? i18n("OpenPGP Certificates") + QLatin1String(" (*.asc *.gpg *.pgp)") : i18n("S/MIME Certificates") + QLatin1String(" (*.pem *.der)")); if (!fname.isEmpty() && protocol == GpgME::CMS && fileNames[GpgME::OpenPGP] == fname) { KMessageBox::error(parentWidgetOrView(), i18n("You have to select different filenames for different protocols."), i18n("Export Error")); return false; } const QFileInfo fi(fname); if (fi.suffix().isEmpty()) { fname += protocol == GpgME::OpenPGP ? QStringLiteral(".asc") : QStringLiteral(".pem"); } fileNames[protocol] = fname; ApplicationState::setLastUsedExportDirectory(fi.absolutePath()); return !fname.isEmpty(); } void ExportCertificateCommand::Private::startExportJob(GpgME::Protocol protocol, const std::vector &keys) { Q_ASSERT(protocol != GpgME::UnknownProtocol); const QGpgME::Protocol *const backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); Q_ASSERT(backend); const QString fileName = fileNames[protocol]; const bool binary = protocol == GpgME::OpenPGP ? fileName.endsWith(QLatin1String(".gpg"), Qt::CaseInsensitive) || fileName.endsWith(QLatin1String(".pgp"), Qt::CaseInsensitive) : fileName.endsWith(QLatin1String(".der"), Qt::CaseInsensitive); std::unique_ptr job(backend->publicKeyExportJob(!binary)); Q_ASSERT(job.get()); connect(job.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &result, const QByteArray &keyData) { exportResult(result, keyData); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(job.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif QStringList fingerprints; fingerprints.reserve(keys.size()); for (const Key &i : keys) { fingerprints << QLatin1String(i.primaryFingerprint()); } const GpgME::Error err = job->start(fingerprints); if (err) { showError(err); finished(); return; } Q_EMIT q->info(i18n("Exporting certificates...")); ++jobsPending; const QPointer exportJob(job.release()); outFileForSender[exportJob.data()] = fileName; (protocol == CMS ? cmsJob : pgpJob) = exportJob; } void ExportCertificateCommand::Private::showError(const GpgME::Error &err) { Q_ASSERT(err); const QString msg = i18n("

An error occurred while trying to export " "the certificate:

" "

%1

", - QString::fromLocal8Bit(err.asString())); + Formatting::errorAsString(err)); error(msg, i18n("Certificate Export Failed")); } void ExportCertificateCommand::doCancel() { d->cancelJobs(); } void ExportCertificateCommand::Private::finishedIfLastJob() { if (jobsPending <= 0) { finished(); } } static bool write_complete(QIODevice &iod, const QByteArray &data) { qint64 total = 0; qint64 toWrite = data.size(); while (total < toWrite) { const qint64 written = iod.write(data.data() + total, toWrite); if (written < 0) { return false; } total += written; toWrite -= written; } return true; } void ExportCertificateCommand::Private::exportResult(const GpgME::Error &err, const QByteArray &data) { Q_ASSERT(jobsPending > 0); --jobsPending; Q_ASSERT(outFileForSender.contains(q->sender())); const QString outFile = outFileForSender[q->sender()]; if (err) { showError(err); finishedIfLastJob(); return; } QSaveFile savefile(outFile); //TODO: use KIO const QString writeErrorMsg = i18n("Could not write to file %1.", outFile); const QString errorCaption = i18n("Certificate Export Failed"); if (!savefile.open(QIODevice::WriteOnly)) { error(writeErrorMsg, errorCaption); finishedIfLastJob(); return; } if (!write_complete(savefile, data) || !savefile.commit()) { error(writeErrorMsg, errorCaption); } finishedIfLastJob(); } void ExportCertificateCommand::Private::cancelJobs() { if (cmsJob) { cmsJob->slotCancel(); } if (pgpJob) { pgpJob->slotCancel(); } } #undef d #undef q #include "moc_exportcertificatecommand.cpp" diff --git a/src/commands/exportgroupscommand.cpp b/src/commands/exportgroupscommand.cpp index c63fdcc8b..6aa94a37f 100644 --- a/src/commands/exportgroupscommand.cpp +++ b/src/commands/exportgroupscommand.cpp @@ -1,302 +1,303 @@ /* -*- mode: c++; c-basic-offset:4 -*- exportgroupscommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "exportgroupscommand.h" #include "command_p.h" #include #include "utils/filedialog.h" #include +#include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; using namespace QGpgME; namespace { static const QString certificateGroupFileExtension{QLatin1String{".kgrp"}}; QString proposeFilename(const std::vector &groups) { QString filename; filename = ApplicationState::lastUsedExportDirectory() + QLatin1Char{'/'}; if (groups.size() == 1) { filename += groups.front().name().replace(QLatin1Char{'/'}, QLatin1Char{'_'}); } else { filename += i18nc("A generic filename for exported certificate groups", "certificate groups"); } return filename + certificateGroupFileExtension; } QString requestFilename(QWidget *parent, const std::vector &groups) { const QString proposedFilename = proposeFilename(groups); auto filename = FileDialog::getSaveFileNameEx( parent, i18ncp("@title:window", "Export Certificate Group", "Export Certificate Groups", groups.size()), QStringLiteral("imp"), proposedFilename, i18nc("filename filter like Certificate Groups (*.foo)", "Certificate Groups (*%1)", certificateGroupFileExtension)); if (!filename.isEmpty()) { const QFileInfo fi{filename}; if (fi.suffix().isEmpty()) { filename += certificateGroupFileExtension; } ApplicationState::setLastUsedExportDirectory(filename); } return filename; } } class ExportGroupsCommand::Private : public Command::Private { friend class ::ExportGroupsCommand; ExportGroupsCommand *q_func() const { return static_cast(q); } public: explicit Private(ExportGroupsCommand *qq); ~Private() override; void start(); bool exportGroups(); bool startExportJob(GpgME::Protocol protocol, const std::vector &keys); void onExportJobResult(const QGpgME::Job *job, const GpgME::Error &err, const QByteArray &keyData); void cancelJobs(); void showError(const GpgME::Error &err); void finishedIfLastJob(const QGpgME::Job *job); private: std::vector groups; QString filename; std::vector> exportJobs; }; ExportGroupsCommand::Private *ExportGroupsCommand::d_func() { return static_cast(d.get()); } const ExportGroupsCommand::Private *ExportGroupsCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ExportGroupsCommand::Private::Private(ExportGroupsCommand *qq) : Command::Private(qq) { } ExportGroupsCommand::Private::~Private() = default; void ExportGroupsCommand::Private::start() { if (groups.empty()) { finished(); return; } filename = requestFilename(parentWidgetOrView(), groups); if (filename.isEmpty()) { canceled(); return; } const auto groupKeys = std::accumulate(std::begin(groups), std::end(groups), KeyGroup::Keys{}, [](auto allKeys, const auto &group) { const auto keys = group.keys(); allKeys.insert(std::begin(keys), std::end(keys)); return allKeys; }); std::vector openpgpKeys; std::vector cmsKeys; std::partition_copy(std::begin(groupKeys), std::end(groupKeys), std::back_inserter(openpgpKeys), std::back_inserter(cmsKeys), [](const GpgME::Key &key) { return key.protocol() == GpgME::OpenPGP; }); // remove/overwrite existing file if (QFile::exists(filename) && !QFile::remove(filename)) { error(xi18n("Cannot overwrite existing %1.", filename), i18nc("@title:window", "Export Failed")); finished(); return; } if (!exportGroups()) { finished(); return; } if (!openpgpKeys.empty()) { if (!startExportJob(GpgME::OpenPGP, openpgpKeys)) { finished(); return; } } if (!cmsKeys.empty()) { if (!startExportJob(GpgME::CMS, cmsKeys)) { finishedIfLastJob(nullptr); } } } bool ExportGroupsCommand::Private::exportGroups() { const auto result = writeKeyGroups(filename, groups); if (result != WriteKeyGroups::Success) { error(xi18n("Writing groups to file %1 failed.", filename), i18nc("@title:window", "Export Failed")); } return result == WriteKeyGroups::Success; } bool ExportGroupsCommand::Private::startExportJob(GpgME::Protocol protocol, const std::vector &keys) { const QGpgME::Protocol *const backend = (protocol == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); Q_ASSERT(backend); std::unique_ptr jobOwner(backend->publicKeyExportJob(/*armor=*/ true)); auto job = jobOwner.get(); Q_ASSERT(job); connect(job, &ExportJob::result, q, [this, job](const GpgME::Error &err, const QByteArray &keyData) { onExportJobResult(job, err, keyData); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(job, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif const GpgME::Error err = job->start(Kleo::getFingerprints(keys)); if (err) { showError(err); return false; } Q_EMIT q->info(i18n("Exporting certificate groups...")); exportJobs.push_back(jobOwner.release()); return true; } void ExportGroupsCommand::Private::onExportJobResult(const QGpgME::Job *job, const GpgME::Error &err, const QByteArray &keyData) { Q_ASSERT(Kleo::contains(exportJobs, job)); if (err) { showError(err); finishedIfLastJob(job); return; } QFile f{filename}; if (!f.open(QIODevice::WriteOnly | QIODevice::Append)) { error(xi18n("Cannot open file %1 for writing.", filename), i18nc("@title:window", "Export Failed")); finishedIfLastJob(job); return; } const auto bytesWritten = f.write(keyData); if (bytesWritten != keyData.size()) { error(xi18n("Writing certificates to file %1 failed.", filename), i18nc("@title:window", "Export Failed")); } finishedIfLastJob(job); } void ExportGroupsCommand::Private::showError(const GpgME::Error &err) { error(xi18n("An error occurred during the export:" "%1", - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), i18nc("@title:window", "Export Failed")); } void ExportGroupsCommand::Private::finishedIfLastJob(const QGpgME::Job *job) { if (job) { exportJobs.erase(std::remove(exportJobs.begin(), exportJobs.end(), job), exportJobs.end()); } if (exportJobs.size() == 0) { finished(); } } void ExportGroupsCommand::Private::cancelJobs() { std::for_each(std::cbegin(exportJobs), std::cend(exportJobs), [](const auto &job) { if (job) { job->slotCancel(); } }); exportJobs.clear(); } ExportGroupsCommand::ExportGroupsCommand(const std::vector &groups) : Command{new Private{this}} { d->groups = groups; } ExportGroupsCommand::~ExportGroupsCommand() = default; void ExportGroupsCommand::doStart() { d->start(); } void ExportGroupsCommand::doCancel() { d->cancelJobs(); } #undef d #undef q #include "moc_exportgroupscommand.cpp" diff --git a/src/commands/exportsecretkeycommand.cpp b/src/commands/exportsecretkeycommand.cpp index cf8cf9ee9..15cfdc0aa 100644 --- a/src/commands/exportsecretkeycommand.cpp +++ b/src/commands/exportsecretkeycommand.cpp @@ -1,316 +1,316 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/exportsecretkeycommand.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 #include "exportsecretkeycommand.h" #include "command_p.h" #include "fileoperationspreferences.h" #include #include "utils/filedialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; namespace { QString openPGPCertificateFileExtension() { return QLatin1String{outputFileExtension(Class::OpenPGP | Class::Ascii | Class::Certificate, FileOperationsPreferences().usePGPFileExt())}; } QString cmsCertificateFileExtension() { return QLatin1String{outputFileExtension(Class::CMS | Class::Binary | Class::ExportedPSM, /*usePGPFileExt=*/false)}; } QString certificateFileExtension(GpgME::Protocol protocol) { switch (protocol) { case GpgME::OpenPGP: return openPGPCertificateFileExtension(); case GpgME::CMS: return cmsCertificateFileExtension(); default: qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Unknown protocol" << protocol; return QStringLiteral("txt"); } } QString proposeFilename(const Key &key) { QString filename; auto name = Formatting::prettyName(key); if (name.isEmpty()) { name = Formatting::prettyEMail(key); } const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID()); /* Not translated so it's better to use in tutorials etc. */ filename = QStringView{u"%1_%2_SECRET"}.arg(name, shortKeyID); filename.replace(u'/', u'_'); return ApplicationState::lastUsedExportDirectory() + u'/' + filename + u'.' + certificateFileExtension(key.protocol()); } QString secretKeyFileFilters(GpgME::Protocol protocol) { switch (protocol) { case GpgME::OpenPGP: return i18nc("description of filename filter", "Secret Key Files") + QLatin1String{" (*.asc *.gpg *.pgp)"}; case GpgME::CMS: return i18nc("description of filename filter", "Secret Key Files") + QLatin1String{" (*.p12)"}; default: qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Unknown protocol" << protocol; return i18nc("description of filename filter", "All Files") + QLatin1String{" (*)"}; } } QString requestFilename(const Key &key, const QString &proposedFilename, QWidget *parent) { auto filename = FileDialog::getSaveFileNameEx( parent, i18nc("@title:window", "Secret Key Backup"), QStringLiteral("imp"), proposedFilename, secretKeyFileFilters(key.protocol())); if (!filename.isEmpty()) { const QFileInfo fi{filename}; if (fi.suffix().isEmpty()) { filename += u'.' + certificateFileExtension(key.protocol()); } ApplicationState::setLastUsedExportDirectory(filename); } return filename; } QString errorCaption() { return i18nc("@title:window", "Secret Key Backup Error"); } } class ExportSecretKeyCommand::Private : public Command::Private { friend class ::ExportSecretKeyCommand; ExportSecretKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(ExportSecretKeyCommand *qq, KeyListController *c = nullptr); ~Private() override; void start(); void cancel(); private: std::unique_ptr startExportJob(const Key &key); void onExportJobResult(const Error &err, const QByteArray &keyData); void showError(const Error &err); private: QString filename; QPointer job; }; ExportSecretKeyCommand::Private *ExportSecretKeyCommand::d_func() { return static_cast(d.get()); } const ExportSecretKeyCommand::Private *ExportSecretKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ExportSecretKeyCommand::Private::Private(ExportSecretKeyCommand *qq, KeyListController *c) : Command::Private{qq, c} { } ExportSecretKeyCommand::Private::~Private() = default; void ExportSecretKeyCommand::Private::start() { const Key key = this->key(); if (key.isNull()) { finished(); return; } filename = requestFilename(key, proposeFilename(key), parentWidgetOrView()); if (filename.isEmpty()) { canceled(); return; } auto exportJob = startExportJob(key); if (!exportJob) { finished(); return; } job = exportJob.release(); } void ExportSecretKeyCommand::Private::cancel() { if (job) { job->slotCancel(); } job.clear(); } std::unique_ptr ExportSecretKeyCommand::Private::startExportJob(const Key &key) { #if QGPGME_SUPPORTS_SECRET_KEY_EXPORT const bool armor = key.protocol() == GpgME::OpenPGP && filename.endsWith(u".asc", Qt::CaseInsensitive); const QGpgME::Protocol *const backend = (key.protocol() == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); Q_ASSERT(backend); std::unique_ptr exportJob{backend->secretKeyExportJob(armor)}; Q_ASSERT(exportJob); if (key.protocol() == GpgME::CMS) { exportJob->setExportFlags(GpgME::Context::ExportPKCS12); } connect(exportJob.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &err, const QByteArray &keyData) { onExportJobResult(err, keyData); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(exportJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(exportJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif const GpgME::Error err = exportJob->start({QLatin1String{key.primaryFingerprint()}}); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Backing up secret key...")); return exportJob; #else Q_UNUSED(key) return {}; #endif } void ExportSecretKeyCommand::Private::onExportJobResult(const Error &err, const QByteArray &keyData) { if (err.isCanceled()) { finished(); return; } if (err) { showError(err); finished(); return; } if (keyData.isEmpty()) { error(i18nc("@info", "The result of the backup is empty. Maybe you entered an empty or a wrong passphrase."), errorCaption()); finished(); return; } QFile f{filename}; if (!f.open(QIODevice::WriteOnly)) { error(xi18nc("@info", "Cannot open file %1 for writing.", filename), errorCaption()); finished(); return; } const auto bytesWritten = f.write(keyData); if (bytesWritten != keyData.size()) { error(xi18nc("@info", "Writing key to file %1 failed.", filename), errorCaption()); finished(); return; } information(i18nc("@info", "The backup of the secret key was created successfully."), i18nc("@title:window", "Secret Key Backup")); finished(); } void ExportSecretKeyCommand::Private::showError(const Error &err) { error(xi18nc("@info", "An error occurred during the backup of the secret key:" "%1", - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), errorCaption()); } ExportSecretKeyCommand::ExportSecretKeyCommand(QAbstractItemView *view, KeyListController *controller) : Command{view, new Private{this, controller}} { } ExportSecretKeyCommand::ExportSecretKeyCommand(const GpgME::Key &key) : Command{key, new Private{this}} { } ExportSecretKeyCommand::~ExportSecretKeyCommand() = default; void ExportSecretKeyCommand::doStart() { d->start(); } void ExportSecretKeyCommand::doCancel() { d->cancel(); } #undef d #undef q #include "moc_exportsecretkeycommand.cpp" diff --git a/src/commands/exportsecretsubkeycommand.cpp b/src/commands/exportsecretsubkeycommand.cpp index 88db09164..86a11d80e 100644 --- a/src/commands/exportsecretsubkeycommand.cpp +++ b/src/commands/exportsecretsubkeycommand.cpp @@ -1,298 +1,298 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/exportsecretsubkeycommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "exportsecretsubkeycommand.h" #include "command_p.h" #include "fileoperationspreferences.h" #include #if QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT #include "utils/filedialog.h" #endif #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; namespace { #if QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT QString openPGPCertificateFileExtension() { return QLatin1String{outputFileExtension(Class::OpenPGP | Class::Ascii | Class::Certificate, FileOperationsPreferences().usePGPFileExt())}; } QString proposeFilename(const std::vector &subkeys) { QString filename; if (subkeys.size() == 1) { const auto subkey = subkeys.front(); const auto key = subkey.parent(); auto name = Formatting::prettyName(key); if (name.isEmpty()) { name = Formatting::prettyEMail(key); } const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID()); const auto shortSubkeyID = Formatting::prettyKeyID(QByteArray{subkey.keyID()}.right(8).constData()); const auto usage = Formatting::usageString(subkey).replace(QLatin1String{", "}, QLatin1String{"_"}); /* Not translated so it's better to use in tutorials etc. */ filename = QStringView{u"%1_%2_SECRET_SUBKEY_%3_%4"}.arg( name, shortKeyID, shortSubkeyID, usage); } else { filename = i18nc("Generic filename for exported subkeys", "subkeys"); } filename.replace(u'/', u'_'); return ApplicationState::lastUsedExportDirectory() + u'/' + filename + u'.' + openPGPCertificateFileExtension(); } QString requestFilename(const std::vector &subkeys, const QString &proposedFilename, QWidget *parent) { auto filename = FileDialog::getSaveFileNameEx( parent, i18ncp("@title:window", "Export Subkey", "Export Subkeys", subkeys.size()), QStringLiteral("imp"), proposedFilename, i18nc("description of filename filter", "Secret Key Files") + QLatin1String{" (*.asc *.gpg *.pgp)"}); if (!filename.isEmpty()) { const QFileInfo fi{filename}; if (fi.suffix().isEmpty()) { filename += u'.' + openPGPCertificateFileExtension(); } ApplicationState::setLastUsedExportDirectory(filename); } return filename; } template QStringList getSubkeyFingerprints(const SubkeyContainer &subkeys) { QStringList fingerprints; fingerprints.reserve(subkeys.size()); std::transform(std::begin(subkeys), std::end(subkeys), std::back_inserter(fingerprints), [](const auto &subkey) { return QLatin1String{subkey.fingerprint()} + u'!'; }); return fingerprints; } #endif } class ExportSecretSubkeyCommand::Private : public Command::Private { friend class ::ExportSecretSubkeyCommand; ExportSecretSubkeyCommand *q_func() const { return static_cast(q); } public: explicit Private(ExportSecretSubkeyCommand *qq); ~Private() override; void start(); void cancel(); private: std::unique_ptr startExportJob(const std::vector &subkeys); void onExportJobResult(const Error &err, const QByteArray &keyData); void showError(const Error &err); private: std::vector subkeys; QString filename; QPointer job; }; ExportSecretSubkeyCommand::Private *ExportSecretSubkeyCommand::d_func() { return static_cast(d.get()); } const ExportSecretSubkeyCommand::Private *ExportSecretSubkeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ExportSecretSubkeyCommand::Private::Private(ExportSecretSubkeyCommand *qq) : Command::Private{qq} { } ExportSecretSubkeyCommand::Private::~Private() = default; void ExportSecretSubkeyCommand::Private::start() { #if QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT if (subkeys.empty()) { finished(); return; } filename = requestFilename(subkeys, proposeFilename(subkeys), parentWidgetOrView()); if (filename.isEmpty()) { canceled(); return; } auto exportJob = startExportJob(subkeys); if (!exportJob) { finished(); return; } job = exportJob.release(); #else Q_ASSERT(!"This command is not supported by the backend it was compiled against"); finished(); return; #endif } void ExportSecretSubkeyCommand::Private::cancel() { if (job) { job->slotCancel(); } job.clear(); } std::unique_ptr ExportSecretSubkeyCommand::Private::startExportJob(const std::vector &subkeys) { #if QGPGME_SUPPORTS_SECRET_SUBKEY_EXPORT const bool armor = filename.endsWith(u".asc", Qt::CaseInsensitive); std::unique_ptr exportJob{QGpgME::openpgp()->secretSubkeyExportJob(armor)}; Q_ASSERT(exportJob); connect(exportJob.get(), &QGpgME::ExportJob::result, q, [this](const GpgME::Error &err, const QByteArray &keyData) { onExportJobResult(err, keyData); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(exportJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(exportJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif const GpgME::Error err = exportJob->start(getSubkeyFingerprints(subkeys)); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Exporting subkeys...")); return exportJob; #else Q_UNUSED(subkeys) return {}; #endif } void ExportSecretSubkeyCommand::Private::onExportJobResult(const Error &err, const QByteArray &keyData) { if (err) { showError(err); finished(); return; } if (keyData.isEmpty()) { error(i18nc("@info", "The result of the export is empty."), i18nc("@title:window", "Export Failed")); finished(); return; } QFile f{filename}; if (!f.open(QIODevice::WriteOnly)) { error(xi18nc("@info", "Cannot open file %1 for writing.", filename), i18nc("@title:window", "Export Failed")); finished(); return; } const auto bytesWritten = f.write(keyData); if (bytesWritten != keyData.size()) { error(xi18ncp("@info", "Writing subkey to file %2 failed.", "Writing subkeys to file %2 failed.", subkeys.size(), filename), i18nc("@title:window", "Export Failed")); finished(); return; } information(i18ncp("@info", "The subkey was exported successfully.", "%1 subkeys were exported successfully.", subkeys.size()), i18nc("@title:window", "Secret Key Backup")); finished(); } void ExportSecretSubkeyCommand::Private::showError(const Error &err) { error(xi18nc("@info", "An error occurred during the export:" "%1", - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), i18nc("@title:window", "Export Failed")); } ExportSecretSubkeyCommand::ExportSecretSubkeyCommand(const std::vector &subkeys) : Command{new Private{this}} { d->subkeys = subkeys; } ExportSecretSubkeyCommand::~ExportSecretSubkeyCommand() = default; void ExportSecretSubkeyCommand::doStart() { d->start(); } void ExportSecretSubkeyCommand::doCancel() { d->cancel(); } #undef d #undef q #include "moc_exportsecretsubkeycommand.cpp" diff --git a/src/commands/importcertificatescommand.cpp b/src/commands/importcertificatescommand.cpp index d74a2b4ef..eaabb35a6 100644 --- a/src/commands/importcertificatescommand.cpp +++ b/src/commands/importcertificatescommand.cpp @@ -1,1139 +1,1139 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/importcertificatescommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007, 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "importcertificatescommand.h" #include "importcertificatescommand_p.h" #include "certifycertificatecommand.h" #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QGPGME_SUPPORTS_RECEIVING_KEYS_BY_KEY_ID #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for Qt::escape #include #include #include #include using namespace GpgME; using namespace Kleo; using namespace QGpgME; static void disconnectConnection(const QMetaObject::Connection &connection) { // trivial function for disconnecting a signal-slot connection QObject::disconnect(connection); } bool operator==(const ImportJobData &lhs, const ImportJobData &rhs) { return lhs.job == rhs.job; } namespace { make_comparator_str(ByImportFingerprint, .fingerprint()); class ImportResultProxyModel : public AbstractKeyListSortFilterProxyModel { Q_OBJECT public: ImportResultProxyModel(const std::vector &results, QObject *parent = nullptr) : AbstractKeyListSortFilterProxyModel(parent) { updateFindCache(results); } ~ImportResultProxyModel() override {} ImportResultProxyModel *clone() const override { // compiler-generated copy ctor is fine! return new ImportResultProxyModel(*this); } void setImportResults(const std::vector &results) { updateFindCache(results); invalidateFilter(); } protected: QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid() || role != Qt::ToolTipRole) { return AbstractKeyListSortFilterProxyModel::data(index, role); } const QString fpr = index.data(KeyList::FingerprintRole).toString(); // find information: const std::vector::const_iterator it = Kleo::binary_find(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint()); if (it == m_importsByFingerprint.end()) { return AbstractKeyListSortFilterProxyModel::data(index, role); } else { QStringList rv; const auto ids = m_idsByFingerprint[it->fingerprint()]; rv.reserve(ids.size()); std::copy(ids.cbegin(), ids.cend(), std::back_inserter(rv)); return Formatting::importMetaData(*it, rv); } } bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { // // 0. Keep parents of matching children: // const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); Q_ASSERT(index.isValid()); for (int i = 0, end = sourceModel()->rowCount(index); i != end; ++i) if (filterAcceptsRow(i, index)) { return true; } // // 1. Check that this is an imported key: // const QString fpr = index.data(KeyList::FingerprintRole).toString(); return std::binary_search(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), fpr.toLatin1().constData(), ByImportFingerprint()); } private: void updateFindCache(const std::vector &results) { m_importsByFingerprint.clear(); m_idsByFingerprint.clear(); m_results = results; for (const auto &r : results) { const std::vector imports = r.result.imports(); m_importsByFingerprint.insert(m_importsByFingerprint.end(), imports.begin(), imports.end()); for (std::vector::const_iterator it = imports.begin(), end = imports.end(); it != end; ++it) { m_idsByFingerprint[it->fingerprint()].insert(r.id); } } std::sort(m_importsByFingerprint.begin(), m_importsByFingerprint.end(), ByImportFingerprint()); } private: mutable std::vector m_importsByFingerprint; mutable std::map< const char *, std::set, ByImportFingerprint > m_idsByFingerprint; std::vector m_results; }; bool importFailed(const ImportResultData &r) { // ignore GPG_ERR_EOF error to handle the "failed" import of files // without X.509 certificates by gpgsm gracefully return r.result.error() && r.result.error().code() != GPG_ERR_EOF; } bool importWasCanceled(const ImportResultData &r) { return r.result.error().isCanceled(); } } ImportCertificatesCommand::Private::Private(ImportCertificatesCommand *qq, KeyListController *c) : Command::Private(qq, c) , progressWindowTitle{i18nc("@title:window", "Importing Certificates")} , progressLabelText{i18n("Importing certificates... (this can take a while)")} { } ImportCertificatesCommand::Private::~Private() { if (progressDialog) { delete progressDialog; } } #define d d_func() #define q q_func() ImportCertificatesCommand::ImportCertificatesCommand(KeyListController *p) : Command(new Private(this, p)) { } ImportCertificatesCommand::ImportCertificatesCommand(QAbstractItemView *v, KeyListController *p) : Command(v, new Private(this, p)) { } ImportCertificatesCommand::~ImportCertificatesCommand() = default; static QString format_ids(const std::vector &ids) { QStringList escapedIds; for (const QString &id : ids) { if (!id.isEmpty()) { escapedIds << id.toHtmlEscaped(); } } return escapedIds.join(QLatin1String("
")); } static QString make_tooltip(const std::vector &results) { if (results.empty()) { return {}; } std::vector ids; ids.reserve(results.size()); std::transform(std::begin(results), std::end(results), std::back_inserter(ids), [](const auto &r) { return r.id; }); std::sort(std::begin(ids), std::end(ids)); ids.erase(std::unique(std::begin(ids), std::end(ids)), std::end(ids)); if (ids.size() == 1) if (ids.front().isEmpty()) { return {}; } else return i18nc("@info:tooltip", "Imported Certificates from %1", ids.front().toHtmlEscaped()); else return i18nc("@info:tooltip", "Imported certificates from these sources:
%1", format_ids(ids)); } void ImportCertificatesCommand::Private::setImportResultProxyModel(const std::vector &results) { if (std::none_of(std::begin(results), std::end(results), [](const auto &r) { return r.result.numConsidered() > 0; })) { return; } q->addTemporaryView(i18nc("@title:tab", "Imported Certificates"), new ImportResultProxyModel(results), make_tooltip(results)); if (QTreeView *const tv = qobject_cast(parentWidgetOrView())) { tv->expandAll(); } } int sum(const std::vector &res, int (ImportResult::*fun)() const) { return kdtools::accumulate_transform(res.begin(), res.end(), std::mem_fn(fun), 0); } static QString make_report(const std::vector &results, const std::vector &groups) { const KLocalizedString normalLine = ki18n("%1%2"); const KLocalizedString boldLine = ki18n("%1%2"); const KLocalizedString headerLine = ki18n("%1"); std::vector res; res.reserve(results.size()); std::transform(std::begin(results), std::end(results), std::back_inserter(res), [](const auto &r) { return r.result; }); const auto numProcessedCertificates = sum(res, &ImportResult::numConsidered); QStringList lines; if (numProcessedCertificates > 0 || groups.size() == 0) { lines.push_back(headerLine.subs(i18n("Certificates")).toString()); lines.push_back(normalLine.subs(i18n("Total number processed:")) .subs(numProcessedCertificates).toString()); lines.push_back(normalLine.subs(i18n("Imported:")) .subs(sum(res, &ImportResult::numImported)).toString()); if (const int n = sum(res, &ImportResult::newSignatures)) lines.push_back(normalLine.subs(i18n("New signatures:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::newUserIDs)) lines.push_back(normalLine.subs(i18n("New user IDs:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::numKeysWithoutUserID)) lines.push_back(normalLine.subs(i18n("Certificates without user IDs:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::newSubkeys)) lines.push_back(normalLine.subs(i18n("New subkeys:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::newRevocations)) lines.push_back(boldLine.subs(i18n("Newly revoked:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::notImported)) lines.push_back(boldLine.subs(i18n("Not imported:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::numUnchanged)) lines.push_back(normalLine.subs(i18n("Unchanged:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysConsidered)) lines.push_back(normalLine.subs(i18n("Secret keys processed:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysImported)) lines.push_back(normalLine.subs(i18n("Secret keys imported:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysConsidered) - sum(res, &ImportResult::numSecretKeysImported) - sum(res, &ImportResult::numSecretKeysUnchanged)) if (n > 0) lines.push_back(boldLine.subs(i18n("Secret keys not imported:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::numSecretKeysUnchanged)) lines.push_back(normalLine.subs(i18n("Secret keys unchanged:")) .subs(n).toString()); if (const int n = sum(res, &ImportResult::numV3KeysSkipped)) lines.push_back(normalLine.subs(i18n("Deprecated PGP-2 keys skipped:")) .subs(n).toString()); } if (!lines.empty()) { lines.push_back(headerLine.subs(QLatin1String{" "}).toString()); } if (groups.size() > 0) { const auto newGroups = std::count_if(std::begin(groups), std::end(groups), [](const auto &g) { return g.status == ImportedGroup::Status::New; }); const auto updatedGroups = groups.size() - newGroups; lines.push_back(headerLine.subs(i18n("Certificate Groups")).toString()); lines.push_back(normalLine.subs(i18n("Total number processed:")) .subs(groups.size()).toString()); lines.push_back(normalLine.subs(i18n("New groups:")) .subs(newGroups).toString()); lines.push_back(normalLine.subs(i18n("Updated groups:")) .subs(updatedGroups).toString()); } return lines.join(QLatin1String{}); } static bool isImportFromSingleSource(const std::vector &res) { return (res.size() == 1) || (res.size() == 2 && res[0].id == res[1].id); } static QString make_message_report(const std::vector &res, const std::vector &groups) { QString report{QLatin1String{""}}; if (res.empty()) { report += i18n("No imports (should not happen, please report a bug)."); } else { const QString title = isImportFromSingleSource(res) && !res.front().id.isEmpty() ? i18n("Detailed results of importing %1:", res.front().id) : i18n("Detailed results of import:"); report += QLatin1String{"

"} + title + QLatin1String{"

"}; report += QLatin1String{"

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

"}; } report += QLatin1String{""}; return report; } // Returns false on error, true if please certify was shown. bool ImportCertificatesCommand::Private::showPleaseCertify(const GpgME::Import &imp) { if (!Kleo::userHasCertificationKey()) { qCDebug(KLEOPATRA_LOG) << q << __func__ << "No certification key available"; return false; } const char *fpr = imp.fingerprint(); if (!fpr) { // WTF qCWarning(KLEOPATRA_LOG) << "Import without fingerprint"; return false; } // Exactly one public key imported. Let's see if it is openpgp. We are async here so // we can just fetch it. auto ctx = wrap_unique(GpgME::Context::createForProtocol(GpgME::OpenPGP)); if (!ctx) { // WTF qCWarning(KLEOPATRA_LOG) << "Failed to create OpenPGP proto"; return false; } ctx->addKeyListMode(KeyListMode::WithSecret); GpgME::Error err; const auto key = ctx->key(fpr, err, false); if (key.isNull() || err) { // No such key most likely not OpenPGP return false; } if (!Kleo::canBeCertified(key)) { // key is expired or revoked return false; } if (key.hasSecret()) { qCDebug(KLEOPATRA_LOG) << q << __func__ << "Secret key is available -> skipping certification"; return false; } for (const auto &uid: key.userIDs()) { if (uid.validity() >= GpgME::UserID::Marginal) { // Already marginal so don't bug the user return false; } } const QStringList suggestions = QStringList() << i18n("A phone call to the person.") << i18n("Using a business card.") << i18n("Confirming it on a trusted website."); auto sel = KMessageBox::questionTwoActions(parentWidgetOrView(), i18n("In order to mark the certificate as valid it needs to be certified.") + QStringLiteral("
") + i18n("Certifying means that you check the Fingerprint.") + QStringLiteral("
") + i18n("Some suggestions to do this are:") + QStringLiteral("
    • %1").arg(suggestions.join(QStringLiteral("
      "))) + QStringLiteral("
  • ") + i18n("Do you wish to start this process now?"), i18nc("@title", "You have imported a new certificate (public key)"), KGuiItem(i18nc("@action:button", "Certify")), KStandardGuiItem::cancel(), QStringLiteral("CertifyQuestion")); if (sel == KMessageBox::ButtonCode::PrimaryAction) { QEventLoop loop; auto cmd = new Commands::CertifyCertificateCommand(key); cmd->setParentWidget(parentWidgetOrView()); connect(cmd, &Command::finished, &loop, &QEventLoop::quit); QMetaObject::invokeMethod(cmd, &Commands::CertifyCertificateCommand::start, Qt::QueuedConnection); loop.exec(); } return true; } namespace { /** * Returns the Import of an OpenPGP key, if a single certificate was imported and this was an OpenPGP key. * Otherwise, returns a null Import. */ auto getSingleOpenPGPImport(const std::vector &res) { static const Import nullImport; if (!isImportFromSingleSource(res)) { return nullImport; } const auto numImported = std::accumulate(res.cbegin(), res.cend(), 0, [](auto s, const auto &r) { return s + r.result.numImported(); }); if (numImported > 1) { return nullImport; } if ((res.size() >= 1) && (res[0].protocol == GpgME::OpenPGP) && (res[0].result.numImported() == 1) && (res[0].result.imports().size() == 1)) { return res[0].result.imports()[0]; } else if ((res.size() == 2) && (res[1].protocol == GpgME::OpenPGP) && (res[1].result.numImported() == 1) && (res[1].result.imports().size() == 1)) { return res[1].result.imports()[0]; } return nullImport; } auto consolidatedAuditLogEntries(const std::vector &res) { static const QString gpg = QStringLiteral("gpg"); static const QString gpgsm = QStringLiteral("gpgsm"); if (res.size() == 1) { return res.front().auditLog; } QStringList auditLogs; auto extractAndAnnotateAuditLog = [](const ImportResultData &r) { QString s; if (!r.id.isEmpty()) { const auto program = r.protocol == GpgME::OpenPGP ? gpg : gpgsm; const auto headerLine = i18nc("file name (imported with gpg/gpgsm)", "%1 (imported with %2)").arg(r.id, program); s += QStringLiteral("
    %1
    ").arg(headerLine); } if (r.auditLog.error().code() == GPG_ERR_NO_DATA) { s += QStringLiteral("") + i18nc("@info", "Audit log is empty.") + QStringLiteral(""); } else if (r.result.error().isCanceled()) { s += QStringLiteral("") + i18nc("@info", "Import was canceled.") + QStringLiteral(""); } else { s += r.auditLog.text(); } return s; }; std::transform(res.cbegin(), res.cend(), std::back_inserter(auditLogs), extractAndAnnotateAuditLog); return AuditLogEntry{auditLogs.join(QLatin1String{"
    "}), Error{}}; } } void ImportCertificatesCommand::Private::showDetails(const std::vector &res, const std::vector &groups) { const auto singleOpenPGPImport = getSingleOpenPGPImport(res); if (!singleOpenPGPImport.isNull()) { if (showPleaseCertify(singleOpenPGPImport)) { return; } } setImportResultProxyModel(res); MessageBox::information(parentWidgetOrView(), make_message_report(res, groups), consolidatedAuditLogEntries(res), i18n("Certificate Import Result")); } static QString make_error_message(const Error &err, const QString &id) { Q_ASSERT(err); Q_ASSERT(!err.isCanceled()); return id.isEmpty() ? i18n("

    An error occurred while trying " "to import the certificate:

    " "

    %1

    ", - QString::fromLocal8Bit(err.asString())) + Formatting::errorAsString(err)) : i18n("

    An error occurred while trying " "to import the certificate %1:

    " "

    %2

    ", - id, QString::fromLocal8Bit(err.asString())); + id, Formatting::errorAsString(err)); } void ImportCertificatesCommand::Private::showError(const ImportResultData &result) { MessageBox::error(parentWidgetOrView(), make_error_message(result.result.error(), result.id), result.auditLog); } void ImportCertificatesCommand::Private::setWaitForMoreJobs(bool wait) { if (wait == waitForMoreJobs) { return; } waitForMoreJobs = wait; if (!waitForMoreJobs) { tryToFinish(); } } void ImportCertificatesCommand::Private::onImportResult(const ImportResult &result, QGpgME::Job *finishedJob) { if (!finishedJob) { finishedJob = qobject_cast(q->sender()); } Q_ASSERT(finishedJob); qCDebug(KLEOPATRA_LOG) << q << __func__ << finishedJob; auto it = std::find_if(std::begin(runningJobs), std::end(runningJobs), [finishedJob](const auto &job) { return job.job == finishedJob; }); Q_ASSERT(it != std::end(runningJobs)); if (it == std::end(runningJobs)) { qCWarning(KLEOPATRA_LOG) << __func__ << "Error: Finished job not found"; return; } Kleo::for_each(it->connections, &disconnectConnection); it->connections.clear(); increaseProgressValue(); const auto job = *it; addImportResult({job.id, job.protocol, job.type, result, AuditLogEntry::fromJob(finishedJob)}, job); } void ImportCertificatesCommand::Private::addImportResult(const ImportResultData &result, const ImportJobData &job) { - qCDebug(KLEOPATRA_LOG) << q << __func__ << result.id << "Result:" << result.result.error().asString(); + qCDebug(KLEOPATRA_LOG) << q << __func__ << result.id << "Result:" << Formatting::errorAsString(result.result.error()); results.push_back(result); if (importFailed(result)) { showError(result); } if (job.job) { const auto count = std::erase(runningJobs, job); Q_ASSERT(count == 1); } tryToFinish(); } static void handleOwnerTrust(const std::vector &results, QWidget *dialog) { for (const auto &r: results) { if (r.protocol != GpgME::Protocol::OpenPGP) { qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping non-OpenPGP import"; continue; } const auto imports = r.result.imports(); for (const auto &import : imports) { if (!(import.status() & (Import::Status::NewKey | Import::Status::ContainedSecretKey))) { qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping already known imported public key"; continue; } const char *fpr = import.fingerprint(); if (!fpr) { qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping import without fingerprint"; continue; } GpgME::Error err; auto ctx = wrap_unique(Context::createForProtocol(GpgME::Protocol::OpenPGP)); if (!ctx){ qCWarning(KLEOPATRA_LOG) << "Failed to get context"; continue; } ctx->addKeyListMode(KeyListMode::WithSecret); const Key toTrustOwner = ctx->key(fpr, err, false); if (toTrustOwner.isNull() || !toTrustOwner.hasSecret()) { continue; } if (toTrustOwner.ownerTrust() == Key::OwnerTrust::Ultimate) { qCDebug(KLEOPATRA_LOG) << __func__ << "Skipping key with ultimate ownertrust"; continue; } const auto toTrustOwnerUserIDs{toTrustOwner.userIDs()}; // ki18n(" ") as initializer because initializing with empty string leads to // (I18N_EMPTY_MESSAGE) const KLocalizedString uids = std::accumulate(toTrustOwnerUserIDs.cbegin(), toTrustOwnerUserIDs.cend(), KLocalizedString{ki18n(" ")}, [](KLocalizedString temp, const auto &uid) { return kxi18nc("@info", "%1%2").subs(temp).subs(Formatting::prettyNameAndEMail(uid)); }); const QString str = xi18nc("@info", "You have imported a certificate with fingerprint" "%1" "" "and user IDs" "%2" "" "Is this your own certificate?", Formatting::prettyID(fpr), uids); int k = KMessageBox::questionTwoActionsCancel(dialog, str, i18nc("@title:window", "Mark Own Certificate"), KGuiItem{i18nc("@action:button", "Yes, It's Mine")}, KGuiItem{i18nc("@action:button", "No, It's Not Mine")}); if (k == KMessageBox::ButtonCode::PrimaryAction) { //To use the ChangeOwnerTrustJob over //the CryptoBackendFactory const QGpgME::Protocol *const backend = QGpgME::openpgp(); if (!backend){ qCWarning(KLEOPATRA_LOG) << "Failed to get CryptoBackend"; return; } ChangeOwnerTrustJob *const j = backend->changeOwnerTrustJob(); j->start(toTrustOwner, Key::Ultimate); } else if (k == KMessageBox::ButtonCode::Cancel) { // do not bother the user with further "Is this yours?" questions return; } } } } static void validateImportedCertificate(const GpgME::Import &import) { if (const auto fpr = import.fingerprint()) { auto key = KeyCache::instance()->findByFingerprint(fpr); if (!key.isNull()) { // this triggers a keylisting with validation for this certificate key.update(); } else { qCWarning(KLEOPATRA_LOG) << __func__ << "Certificate with fingerprint" << fpr << "not found"; } } } static void handleExternalCMSImports(const std::vector &results) { // For external CMS Imports we have to manually do a keylist // with validation to get the intermediate and root ca imported // automatically if trusted-certs and extra-certs are used. for (const auto &r : results) { if (r.protocol == GpgME::CMS && r.type == ImportType::External && !importFailed(r) && !importWasCanceled(r)) { const auto imports = r.result.imports(); std::for_each(std::begin(imports), std::end(imports), &validateImportedCertificate); } } } void ImportCertificatesCommand::Private::processResults() { importGroups(); #if QGPGME_SUPPORTS_RECEIVING_KEYS_BY_KEY_ID if (Settings{}.retrieveSignerKeysAfterImport() && !importingSignerKeys) { importingSignerKeys = true; const auto missingSignerKeys = getMissingSignerKeyIds(results); if (!missingSignerKeys.empty()) { importSignerKeys(missingSignerKeys); return; } } #endif handleExternalCMSImports(results); // ensure that the progress dialog is closed before we show any other dialogs setProgressToMaximum(); handleOwnerTrust(results, parentWidgetOrView()); showDetails(results, importedGroups); auto tv = dynamic_cast (view()); if (!tv) { qCDebug(KLEOPATRA_LOG) << "Failed to find treeview"; } else { tv->expandAll(); } finished(); } void ImportCertificatesCommand::Private::tryToFinish() { qCDebug(KLEOPATRA_LOG) << q << __func__; if (waitForMoreJobs) { qCDebug(KLEOPATRA_LOG) << q << __func__ << "Waiting for more jobs -> keep going"; return; } if (!runningJobs.empty()) { qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are unfinished jobs -> keep going"; return; } #if QGPGME_SUPPORTS_DEFERRED_IMPORT_JOB if (!pendingJobs.empty()) { qCDebug(KLEOPATRA_LOG) << q << __func__ << "There are pending jobs -> start the next one"; auto job = pendingJobs.front(); pendingJobs.pop(); job.job->startNow(); runningJobs.push_back(job); return; } #endif if (keyListConnection) { qCWarning(KLEOPATRA_LOG) << q << __func__ << "There is already a valid keyListConnection!"; } else { auto keyCache = KeyCache::mutableInstance(); keyListConnection = connect(keyCache.get(), &KeyCache::keyListingDone, q, [this]() { keyCacheUpdated(); }); keyCache->startKeyListing(); } } void ImportCertificatesCommand::Private::keyCacheUpdated() { qCDebug(KLEOPATRA_LOG) << q << __func__; if (!disconnect(keyListConnection)) { qCWarning(KLEOPATRA_LOG) << q << __func__ << "Failed to disconnect keyListConnection"; } keyCacheAutoRefreshSuspension.reset(); const auto allIds = std::accumulate(std::cbegin(results), std::cend(results), std::set{}, [](auto allIds, const auto &r) { allIds.insert(r.id); return allIds; }); const auto canceledIds = std::accumulate(std::cbegin(results), std::cend(results), std::set{}, [](auto canceledIds, const auto &r) { if (importWasCanceled(r)) { canceledIds.insert(r.id); } return canceledIds; }); const auto totalConsidered = std::accumulate(std::cbegin(results), std::cend(results), 0, [](auto totalConsidered, const auto &r) { return totalConsidered + r.result.numConsidered(); }); if (totalConsidered == 0 && canceledIds.size() == allIds.size()) { // nothing was considered for import and at least one import per id was // canceled => treat the command as canceled canceled(); return; } processResults(); } static ImportedGroup storeGroup(const KeyGroup &group, const QString &id) { const auto status = KeyCache::instance()->group(group.id()).isNull() ? ImportedGroup::Status::New : ImportedGroup::Status::Updated; if (status == ImportedGroup::Status::New) { KeyCache::mutableInstance()->insert(group); } else { KeyCache::mutableInstance()->update(group); } return {id, group, status}; } void ImportCertificatesCommand::Private::importGroups() { for (const auto &path : filesToImportGroupsFrom) { const bool certificateImportSucceeded = std::any_of(std::cbegin(results), std::cend(results), [path](const auto &r) { return r.id == path && !importFailed(r) && !importWasCanceled(r); }); if (certificateImportSucceeded) { qCDebug(KLEOPATRA_LOG) << __func__ << "Importing groups from file" << path; const auto groups = readKeyGroups(path); std::transform(std::begin(groups), std::end(groups), std::back_inserter(importedGroups), [path](const auto &group) { return storeGroup(group, path); }); } increaseProgressValue(); } filesToImportGroupsFrom.clear(); } static auto accumulateNewKeys(std::vector &fingerprints, const std::vector &imports) { return std::accumulate(std::begin(imports), std::end(imports), fingerprints, [](auto fingerprints, const auto &import) { if (import.status() == Import::NewKey) { fingerprints.push_back(import.fingerprint()); } return fingerprints; }); } static auto accumulateNewOpenPGPKeys(const std::vector &results) { return std::accumulate(std::begin(results), std::end(results), std::vector{}, [](auto fingerprints, const auto &r) { if (r.protocol == GpgME::OpenPGP) { fingerprints = accumulateNewKeys(fingerprints, r.result.imports()); } return fingerprints; }); } std::set ImportCertificatesCommand::Private::getMissingSignerKeyIds(const std::vector &results) { auto newOpenPGPKeys = KeyCache::instance()->findByFingerprint(accumulateNewOpenPGPKeys(results)); // update all new OpenPGP keys to get information about certifications std::for_each(std::begin(newOpenPGPKeys), std::end(newOpenPGPKeys), std::mem_fn(&Key::update)); auto missingSignerKeyIds = Kleo::getMissingSignerKeyIds(newOpenPGPKeys); return missingSignerKeyIds; } void ImportCertificatesCommand::Private::importSignerKeys(const std::set &keyIds) { Q_ASSERT(!keyIds.empty()); setProgressLabelText(i18np("Fetching 1 signer key... (this can take a while)", "Fetching %1 signer keys... (this can take a while)", keyIds.size())); setWaitForMoreJobs(true); // start one import per key id to allow canceling the key retrieval without // losing already retrieved keys for (const auto &keyId : keyIds) { startImport(GpgME::OpenPGP, {keyId}, QStringLiteral("Retrieve Signer Keys")); } setWaitForMoreJobs(false); } static std::unique_ptr get_import_job(GpgME::Protocol protocol) { Q_ASSERT(protocol != UnknownProtocol); if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) { return std::unique_ptr(backend->importJob()); } else { return std::unique_ptr(); } } void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, const QByteArray &data, const QString &id, [[maybe_unused]] const ImportOptions &options) { Q_ASSERT(protocol != UnknownProtocol); if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) { return; } std::unique_ptr job = get_import_job(protocol); if (!job.get()) { nonWorkingProtocols.push_back(protocol); error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)), i18n("Certificate Import Failed")); addImportResult({id, protocol, ImportType::Local, ImportResult{}, AuditLogEntry{}}); return; } keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh(); std::vector connections = { connect(job.get(), &AbstractImportJob::result, q, [this](const GpgME::ImportResult &result) { onImportResult(result); }), #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress), #else connect(job.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }), #endif }; #if QGPGME_SUPPORTS_IMPORT_WITH_FILTER job->setImportFilter(options.importFilter); #endif #if QGPGME_SUPPORTS_IMPORT_WITH_KEY_ORIGIN job->setKeyOrigin(options.keyOrigin, options.keyOriginUrl); #endif #if QGPGME_SUPPORTS_DEFERRED_IMPORT_JOB const GpgME::Error err = job->startLater(data); #else const GpgME::Error err = job->start(data); #endif if (err.code()) { addImportResult({id, protocol, ImportType::Local, ImportResult{err}, AuditLogEntry{}}); } else { increaseProgressMaximum(); #if QGPGME_SUPPORTS_DEFERRED_IMPORT_JOB pendingJobs.push({id, protocol, ImportType::Local, job.release(), connections}); #else runningJobs.push_back({id, protocol, ImportType::Local, job.release(), connections}); #endif } } static std::unique_ptr get_import_from_keyserver_job(GpgME::Protocol protocol) { Q_ASSERT(protocol != UnknownProtocol); if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) { return std::unique_ptr(backend->importFromKeyserverJob()); } else { return std::unique_ptr(); } } void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, const std::vector &keys, const QString &id) { Q_ASSERT(protocol != UnknownProtocol); if (std::find(nonWorkingProtocols.cbegin(), nonWorkingProtocols.cend(), protocol) != nonWorkingProtocols.cend()) { return; } std::unique_ptr job = get_import_from_keyserver_job(protocol); if (!job.get()) { nonWorkingProtocols.push_back(protocol); error(i18n("The type of this certificate (%1) is not supported by this Kleopatra installation.", Formatting::displayName(protocol)), i18n("Certificate Import Failed")); addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}}); return; } keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh(); std::vector connections = { connect(job.get(), &AbstractImportJob::result, q, [this](const GpgME::ImportResult &result) { onImportResult(result); }), #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress), #else connect(job.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }), #endif }; const GpgME::Error err = job->start(keys); if (err.code()) { addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}}); } else { increaseProgressMaximum(); runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections}); } } static auto get_receive_keys_job(GpgME::Protocol protocol) { Q_ASSERT(protocol != UnknownProtocol); #if QGPGME_SUPPORTS_RECEIVING_KEYS_BY_KEY_ID std::unique_ptr job{}; if (const auto backend = (protocol == GpgME::OpenPGP ? QGpgME::openpgp() : QGpgME::smime())) { job.reset(backend->receiveKeysJob()); } return job; #else return std::unique_ptr{}; #endif } void ImportCertificatesCommand::Private::startImport(GpgME::Protocol protocol, [[maybe_unused]] const QStringList &keyIds, const QString &id) { Q_ASSERT(protocol != UnknownProtocol); auto job = get_receive_keys_job(protocol); if (!job.get()) { qCWarning(KLEOPATRA_LOG) << "Failed to get ReceiveKeysJob for protocol" << Formatting::displayName(protocol); addImportResult({id, protocol, ImportType::External, ImportResult{}, AuditLogEntry{}}); return; } #if QGPGME_SUPPORTS_RECEIVING_KEYS_BY_KEY_ID keyCacheAutoRefreshSuspension = KeyCache::mutableInstance()->suspendAutoRefresh(); std::vector connections = { connect(job.get(), &AbstractImportJob::result, q, [this](const GpgME::ImportResult &result) { onImportResult(result); }), #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job.get(), &QGpgME::Job::jobProgress, q, &Command::progress), #else connect(job.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }), #endif }; const GpgME::Error err = job->start(keyIds); if (err.code()) { addImportResult({id, protocol, ImportType::External, ImportResult{err}, AuditLogEntry{}}); } else { increaseProgressMaximum(); runningJobs.push_back({id, protocol, ImportType::External, job.release(), connections}); } #endif } void ImportCertificatesCommand::Private::importGroupsFromFile(const QString &filename) { increaseProgressMaximum(); filesToImportGroupsFrom.push_back(filename); } void ImportCertificatesCommand::Private::setUpProgressDialog() { if (progressDialog) { return; } progressDialog = new QProgressDialog{parentWidgetOrView()}; // use a non-modal progress dialog to avoid reentrancy problems (and crashes) if multiple jobs finish in the same event loop cycle // (cf. the warning for QProgressDialog::setValue() in the API documentation) progressDialog->setModal(false); progressDialog->setWindowTitle(progressWindowTitle); progressDialog->setLabelText(progressLabelText); progressDialog->setMinimumDuration(1000); progressDialog->setMaximum(1); progressDialog->setValue(0); connect(progressDialog, &QProgressDialog::canceled, q, &Command::cancel); connect(q, &Command::finished, progressDialog, [this]() { progressDialog->accept(); }); } void ImportCertificatesCommand::Private::setProgressWindowTitle(const QString &title) { if (progressDialog) { progressDialog->setWindowTitle(title); } else { progressWindowTitle = title; } } void ImportCertificatesCommand::Private::setProgressLabelText(const QString &text) { if (progressDialog) { progressDialog->setLabelText(text); } else { progressLabelText = text; } } void ImportCertificatesCommand::Private::increaseProgressMaximum() { setUpProgressDialog(); progressDialog->setMaximum(progressDialog->maximum() + 1); qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum(); } void ImportCertificatesCommand::Private::increaseProgressValue() { progressDialog->setValue(progressDialog->value() + 1); qCDebug(KLEOPATRA_LOG) << __func__ << "progress:" << progressDialog->value() << "/" << progressDialog->maximum(); } void ImportCertificatesCommand::Private::setProgressToMaximum() { qCDebug(KLEOPATRA_LOG) << __func__; progressDialog->setValue(progressDialog->maximum()); } void ImportCertificatesCommand::doCancel() { const auto jobsToCancel = d->runningJobs; std::for_each(std::begin(jobsToCancel), std::end(jobsToCancel), [this](const auto &job) { if (!job.connections.empty()) { // ignore jobs without connections; they are already completed qCDebug(KLEOPATRA_LOG) << "Canceling job" << job.job; job.job->slotCancel(); d->onImportResult(ImportResult{Error::fromCode(GPG_ERR_CANCELED)}, job.job); } }); } #undef d #undef q #include "importcertificatescommand.moc" #include "moc_importcertificatescommand.cpp" diff --git a/src/commands/importpaperkeycommand.cpp b/src/commands/importpaperkeycommand.cpp index 18fba732b..6d5e29259 100644 --- a/src/commands/importpaperkeycommand.cpp +++ b/src/commands/importpaperkeycommand.cpp @@ -1,220 +1,221 @@ /* commands/importperkeycommand.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-License-Identifier: GPL-2.0-or-later */ #include #include "importpaperkeycommand.h" +#include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include "command_p.h" using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; ImportPaperKeyCommand::ImportPaperKeyCommand(const GpgME::Key &k) : GnuPGProcessCommand(k) { } QStringList ImportPaperKeyCommand::arguments() const { const Key key = d->key(); QStringList result; result << paperKeyInstallPath() << QStringLiteral("--pubring") << mTmpDir.path() + QStringLiteral("/pubkey.gpg") << QStringLiteral("--secrets") << mTmpDir.path() + QStringLiteral("/secrets.txt") << QStringLiteral("--output") << mTmpDir.path() + QStringLiteral("/seckey.gpg"); return result; } void ImportPaperKeyCommand::exportResult(const GpgME::Error &err, const QByteArray &data) { if (err) { - d->error(QString::fromUtf8(err.asString()), errorCaption()); + d->error(Formatting::errorAsString(err), errorCaption()); d->finished(); return; } if (!mTmpDir.isValid()) { // Should not happen so no i18n d->error(QStringLiteral("Failed to get temporary directory"), errorCaption()); qCWarning(KLEOPATRA_LOG) << "Failed to get temporary dir"; d->finished(); return; } const QString fileName = mTmpDir.path() + QStringLiteral("/pubkey.gpg"); QFile f(fileName); if (!f.open(QIODevice::WriteOnly)) { d->error(QStringLiteral("Failed to create temporary file"), errorCaption()); qCWarning(KLEOPATRA_LOG) << "Failed to open tmp file"; d->finished(); return; } f.write(data); f.close(); // Copy and sanitize input a bit QFile input(mFileName); if (!input.open(QIODevice::ReadOnly)) { d->error(xi18n("Cannot open %1 for reading.", mFileName), errorCaption()); d->finished(); return; } const QString outName = mTmpDir.path() + QStringLiteral("/secrets.txt"); QFile out(outName); if (!out.open(QIODevice::WriteOnly)) { // Should not happen d->error(QStringLiteral("Failed to create temporary file"), errorCaption()); qCWarning(KLEOPATRA_LOG) << "Failed to open tmp file for writing"; d->finished(); return; } QTextStream in(&input); while (!in.atEnd()) { // Paperkey is picky, tabs may not be part. Neither may be empty lines. const QString line = in.readLine().trimmed().replace(QLatin1Char('\t'), QStringLiteral(" ")) + QLatin1Char('\n'); out.write(line.toUtf8()); } input.close(); out.close(); GnuPGProcessCommand::doStart(); } void ImportPaperKeyCommand::postSuccessHook(QWidget *) { qCDebug(KLEOPATRA_LOG) << "Paperkey secrets restore finished successfully."; QFile secKey(mTmpDir.path() + QStringLiteral("/seckey.gpg")); if (!secKey.open(QIODevice::ReadOnly)) { d->error(QStringLiteral("Failed to open temporary secret"), errorCaption()); qCWarning(KLEOPATRA_LOG) << "Failed to open tmp file"; Q_EMIT finished(); return; } auto data = secKey.readAll(); secKey.close(); auto importjob = QGpgME::openpgp()->importJob(); auto result = importjob->exec(data); delete importjob; if (result.error()) { - d->error(QString::fromUtf8(result.error().asString()), errorCaption()); + d->error(Formatting::errorAsString(result.error()), errorCaption()); Q_EMIT finished(); return; } if (!result.numSecretKeysImported() || (result.numSecretKeysUnchanged() == result.numSecretKeysImported())) { d->error(i18n("Failed to restore any secret keys."), errorCaption()); Q_EMIT finished(); return; } // Refresh the key after success KeyCache::mutableInstance()->reload(OpenPGP); Q_EMIT finished(); d->information(xi18nc("@info", "Successfully restored the secret key parts from %1", mFileName)); return; } void ImportPaperKeyCommand::doStart() { if (paperKeyInstallPath().isNull()) { KMessageBox::error(d->parentWidgetOrView(), xi18nc("@info", "Kleopatra uses " "PaperKey to import your " "text backup." "Please make sure it is installed."), i18nc("@title", "Failed to find PaperKey executable.")); return; } mFileName = QFileDialog::getOpenFileName(d->parentWidgetOrView(), i18n("Select input file"), QString(), QStringLiteral("%1 (*.txt)").arg(i18n("Paper backup")) #ifdef Q_OS_WIN /* For whatever reason at least with Qt 5.6.1 the native file dialog crashes in * my (aheinecke) Windows 10 environment when invoked here. * In other places it works, with the same arguments as in other places (e.g. import) * it works. But not here. Maybe it's our (gpg4win) build? But why did it only * crash here? * * It does not crash immediately, the program flow continues for a while before it * crashes so this is hard to debug. * * There are some reports about this * QTBUG-33119 QTBUG-41416 where different people describe "bugs" but they * describe them differently also not really reproducible. * Anyway this works for now and for such an exotic feature its good enough for now. */ , 0, QFileDialog::DontUseNativeDialog #endif ); if (mFileName.isEmpty()) { d->finished(); return; } auto exportJob = QGpgME::openpgp()->publicKeyExportJob(); connect(exportJob, &QGpgME::ExportJob::result, this, &ImportPaperKeyCommand::exportResult); exportJob->start(QStringList() << QLatin1String(d->key().primaryFingerprint())); } QString ImportPaperKeyCommand::errorCaption() const { return i18nc("@title:window", "Error importing secret key"); } QString ImportPaperKeyCommand::crashExitMessage(const QStringList &args) const { return xi18nc("@info", "The GPG process that tried to restore the secret key " "ended prematurely because of an unexpected error." "Please check the output of %1 for details.", args.join(QLatin1Char(' '))); } QString ImportPaperKeyCommand::errorExitMessage(const QStringList &args) const { return xi18nc("@info", "An error occurred while trying to restore the secret key. " "The output from %1 was:" "%2", args[0], errorString()); } QString ImportPaperKeyCommand::successMessage(const QStringList &) const { return QString(); } diff --git a/src/commands/keytocardcommand.cpp b/src/commands/keytocardcommand.cpp index e71cbd207..7c323b9be 100644 --- a/src/commands/keytocardcommand.cpp +++ b/src/commands/keytocardcommand.cpp @@ -1,736 +1,736 @@ /* commands/keytocardcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2020,2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keytocardcommand.h" #include "cardcommand_p.h" #include "authenticatepivcardapplicationcommand.h" #include "smartcard/algorithminfo.h" #include "smartcard/openpgpcard.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "smartcard/utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; namespace { QString cardDisplayName(const std::shared_ptr &card) { return i18nc("smartcard application - serial number of smartcard", "%1 - %2", displayAppName(card->appName()), card->displaySerialNumber()); } } class KeyToCardCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::KeyToCardCommand; KeyToCardCommand *q_func() const { return static_cast(q); } public: explicit Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey); explicit Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName); private: void start(); void startKeyToOpenPGPCard(); Subkey getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &card); void startKeyToPIVCard(); void authenticate(); void authenticationFinished(); void authenticationCanceled(); void keyToCardDone(const GpgME::Error &err); void keyToPIVCardDone(const GpgME::Error &err); void updateDone(); void keyHasBeenCopiedToCard(); bool backupKey(); std::vector readSecretKeyFile(); bool writeSecretKeyBackup(const QString &filename, const std::vector &keydata); void startDeleteSecretKeyLocally(); void deleteSecretKeyLocallyFinished(const GpgME::Error &err); private: std::string appName; GpgME::Subkey subkey; std::string cardSlot; bool overwriteExistingAlreadyApproved = false; bool hasBeenCanceled = false; QMetaObject::Connection updateConnection; }; KeyToCardCommand::Private *KeyToCardCommand::d_func() { return static_cast(d.get()); } const KeyToCardCommand::Private *KeyToCardCommand::d_func() const { return static_cast(d.get()); } #define q q_func() #define d d_func() KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey_) : CardCommand::Private(qq, "", nullptr) , subkey(subkey_) { } KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName_) : CardCommand::Private(qq, serialNumber, nullptr) , appName(appName_) , cardSlot(slot) { } namespace { static std::shared_ptr getCardToTransferSubkeyTo(const Subkey &subkey, QWidget *parent) { const std::vector > suitableCards = KeyToCardCommand::getSuitableCards(subkey); if (suitableCards.empty()) { return std::shared_ptr(); } else if (suitableCards.size() == 1) { return suitableCards[0]; } QStringList options; for (const auto &card: suitableCards) { options.push_back(cardDisplayName(card)); } bool ok; const QString choice = QInputDialog::getItem(parent, i18n("Select Card"), i18n("Please select the card the key should be written to:"), options, /* current= */ 0, /* editable= */ false, &ok); if (!ok) { return std::shared_ptr(); } const int index = options.indexOf(choice); return suitableCards[index]; } } void KeyToCardCommand::Private::start() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::start()"; if (!subkey.isNull() && serialNumber().empty()) { const auto card = getCardToTransferSubkeyTo(subkey, parentWidgetOrView()); if (!card) { finished(); return; } setSerialNumber(card->serialNumber()); appName = card->appName(); } const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (card->appName() == SmartCard::OpenPGPCard::AppName) { startKeyToOpenPGPCard(); } else if (card->appName() == SmartCard::PIVCard::AppName) { startKeyToPIVCard(); } else { error(xi18nc("@info", "Sorry! Writing keys to the card %1 is not supported.", cardDisplayName(card))); finished(); return; } } namespace { static std::string getOpenPGPCardSlotForKey(const GpgME::Subkey &subKey, QWidget *parent) { // Check if we need to ask the user for the slot if ((subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt() && !subKey.canAuthenticate()) { // Signing only return OpenPGPCard::pgpSigKeyRef(); } if (subKey.canEncrypt() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canAuthenticate()) { // Encrypt only return OpenPGPCard::pgpEncKeyRef(); } if (subKey.canAuthenticate() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt()) { // Auth only return OpenPGPCard::pgpAuthKeyRef(); } // Multiple uses, ask user. QStringList options; std::vector cardSlots; if (subKey.canSign() || subKey.canCertify()) { options.push_back(i18nc("@item:inlistbox", "Signature")); cardSlots.push_back(OpenPGPCard::pgpSigKeyRef()); } if (subKey.canEncrypt()) { options.push_back(i18nc("@item:inlistbox", "Encryption")); cardSlots.push_back(OpenPGPCard::pgpEncKeyRef()); } if (subKey.canAuthenticate()) { options.push_back(i18nc("@item:inlistbox", "Authentication")); cardSlots.push_back(OpenPGPCard::pgpAuthKeyRef()); } bool ok; const QString choice = QInputDialog::getItem(parent, i18n("Select Card Slot"), i18n("Please select the card slot the key should be written to:"), options, /* current= */ 0, /* editable= */ false, &ok); const int choiceIndex = options.indexOf(choice); if (ok && choiceIndex >= 0) { return cardSlots[choiceIndex]; } else { return {}; } } } void KeyToCardCommand::Private::startKeyToOpenPGPCard() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToOpenPGPCard()"; const auto pgpCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pgpCard) { error(i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (subkey.isNull()) { finished(); return; } if (subkey.parent().protocol() != GpgME::OpenPGP) { error(i18n("Sorry! This key cannot be transferred to an OpenPGP card.")); finished(); return; } cardSlot = getOpenPGPCardSlotForKey(subkey, parentWidgetOrView()); if (cardSlot.empty()) { finished(); return; } // Check if we need to do the overwrite warning. const std::string existingKey = pgpCard->keyFingerprint(cardSlot); if (!existingKey.empty()) { const auto encKeyWarning = (cardSlot == OpenPGPCard::pgpEncKeyRef()) ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString{}; const QString message = i18nc( "@info", "

    The card %1 already contains a key in this slot. Continuing will overwrite that key.

    " "

    If there is no backup the existing key will be irrecoverably lost.

    ", cardDisplayName(pgpCard)) + i18n("The existing key has the fingerprint:") + QStringLiteral("
    %1
    ").arg(Formatting::prettyID(existingKey.c_str())) + encKeyWarning; const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Overwrite existing key"), KGuiItem{i18nc("@action:button", "Overwrite Existing Key")}, KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { finished(); return; } } // Now do the deed const auto time = QDateTime::fromSecsSinceEpoch(quint32(subkey.creationTime()), Qt::UTC); const auto timestamp = time.toString(QStringLiteral("yyyyMMdd'T'HHmmss")); const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 %3 %4") .arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber()), QString::fromStdString(cardSlot), timestamp); ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) { keyToCardDone(err); }); } namespace { static std::vector getSigningCertificates() { std::vector signingCertificates = KeyCache::instance()->secretKeys(); const auto it = std::remove_if(signingCertificates.begin(), signingCertificates.end(), [](const Key &key) { return ! (key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canSign() && !key.subkey(0).canEncrypt() && key.subkey(0).isSecret() && !key.subkey(0).isCardKey()); }); signingCertificates.erase(it, signingCertificates.end()); return signingCertificates; } static std::vector getEncryptionCertificates() { std::vector encryptionCertificates = KeyCache::instance()->secretKeys(); const auto it = std::remove_if(encryptionCertificates.begin(), encryptionCertificates.end(), [](const Key &key) { return ! (key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canEncrypt() && key.subkey(0).isSecret() && !key.subkey(0).isCardKey()); }); encryptionCertificates.erase(it, encryptionCertificates.end()); return encryptionCertificates; } } Subkey KeyToCardCommand::Private::getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &/*card*/) { if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) { return Subkey(); } const std::vector certificates = cardSlot == PIVCard::cardAuthenticationKeyRef() ? getSigningCertificates() : getEncryptionCertificates(); if (certificates.empty()) { error(i18n("Sorry! No suitable certificate to write to this card slot was found.")); return Subkey(); } auto dialog = new KeySelectionDialog(parentWidgetOrView()); dialog->setWindowTitle(i18nc("@title:window", "Select Certificate")); dialog->setText(i18n("Please select the certificate whose key pair you want to write to the card:")); dialog->setKeys(certificates); if (dialog->exec() == QDialog::Rejected) { return Subkey(); } return dialog->selectedKey().subkey(0); } void KeyToCardCommand::Private::startKeyToPIVCard() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToPIVCard()"; const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) { // key to card is only supported for the Card Authentication key and the Key Management key finished(); return; } if (subkey.isNull()) { subkey = getSubkeyToTransferToPIVCard(cardSlot, pivCard); } if (subkey.isNull()) { finished(); return; } if (subkey.parent().protocol() != GpgME::CMS) { error(i18n("Sorry! This key cannot be transferred to a PIV card.")); finished(); return; } if (!subkey.canEncrypt() && !subkey.canSign()) { error(i18n("Sorry! Only encryption keys and signing keys can be transferred to a PIV card.")); finished(); return; } // Check if we need to do the overwrite warning. if (!overwriteExistingAlreadyApproved) { const std::string existingKey = pivCard->keyInfo(cardSlot).grip; if (!existingKey.empty() && (existingKey != subkey.keyGrip())) { const QString decryptionWarning = (cardSlot == PIVCard::keyManagementKeyRef()) ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString(); const QString message = i18nc( "@info", "

    The card %1 already contains a key in this slot. Continuing will overwrite that key.

    " "

    If there is no backup the existing key will be irrecoverably lost.

    ", cardDisplayName(pivCard)) + i18n("The existing key has the key grip:") + QStringLiteral("
    %1
    ").arg(QString::fromStdString(existingKey)) + decryptionWarning; const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Overwrite existing key"), KGuiItem{i18nc("@action:button", "Overwrite Existing Key")}, KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { finished(); return; } overwriteExistingAlreadyApproved = true; } } const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 %3") .arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber())) .arg(QString::fromStdString(cardSlot)); ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) { keyToPIVCardDone(err); }); } void KeyToCardCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void KeyToCardCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationFinished()"; if (!hasBeenCanceled) { startKeyToPIVCard(); } } void KeyToCardCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } void KeyToCardCommand::Private::updateDone() { disconnect(updateConnection); const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const std::string keyGripOnCard = card->keyInfo(cardSlot).grip; if (keyGripOnCard != subkey.keyGrip()) { qCWarning(KLEOPATRA_LOG) << q << __func__ << "KEYTOCARD succeeded, but key on card doesn't match copied key"; error(i18nc("@info", "Copying the key to the card failed.")); finished(); return; } keyHasBeenCopiedToCard(); } void KeyToCardCommand::Private::keyHasBeenCopiedToCard() { const auto answer = KMessageBox::questionTwoActionsCancel( parentWidgetOrView(), xi18nc("@info", "The key has been copied to the card." "Do you want to delete the copy of the key stored on this computer?"), i18nc("@title:window", "Success"), KGuiItem{i18nc("@action:button", "Create Backup and Delete Key")}, KGuiItem{i18nc("@action:button", "Delete Key")}, KGuiItem{i18nc("@action:button", "Keep Key")}); if (answer == KMessageBox::ButtonCode::Cancel) { finished(); return; } if (answer == KMessageBox::ButtonCode::PrimaryAction) { if (!backupKey()) { finished(); return; } } startDeleteSecretKeyLocally(); } namespace { QString gnupgPrivateKeyBackupExtension() { return QStringLiteral(".gpgsk"); } QString proposeFilename(const Subkey &subkey) { QString filename; const auto key = subkey.parent(); auto name = Formatting::prettyName(key); if (name.isEmpty()) { name = Formatting::prettyEMail(key); } const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID()); const auto shortSubkeyID = Formatting::prettyKeyID(QByteArray{subkey.keyID()}.right(8).constData()); const auto usage = Formatting::usageString(subkey).replace(QLatin1String{", "}, QLatin1String{"_"}); /* Not translated so it's better to use in tutorials etc. */ filename = ((shortKeyID == shortSubkeyID) // ? QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3"}.arg(name, shortKeyID, usage) : QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3_%4"}.arg(name, shortKeyID, shortSubkeyID, usage)); filename.replace(u'/', u'_'); return QDir{ApplicationState::lastUsedExportDirectory()}.filePath(filename + gnupgPrivateKeyBackupExtension()); } QString requestPrivateKeyBackupFilename(const QString &proposedFilename, QWidget *parent) { auto filename = FileDialog::getSaveFileNameEx( parent, i18nc("@title:window", "Backup Secret Key"), QStringLiteral("imp"), proposedFilename, i18nc("description of filename filter", "Secret Key Backup Files") + QLatin1String{" (*.gpgsk)"}); if (!filename.isEmpty()) { const QFileInfo fi{filename}; if (fi.suffix().isEmpty()) { filename += gnupgPrivateKeyBackupExtension(); } ApplicationState::setLastUsedExportDirectory(filename); } return filename; } } bool KeyToCardCommand::Private::backupKey() { static const QByteArray backupInfoName = "Backup-info:"; auto keydata = readSecretKeyFile(); if (keydata.empty()) { return false; } const auto filename = requestPrivateKeyBackupFilename(proposeFilename(subkey), parentWidgetOrView()); if (filename.isEmpty()) { return false; } // remove old backup info Kleo::erase_if(keydata, [](const auto &line) { return line.startsWith(backupInfoName); }); // prepend new backup info const QByteArrayList backupInfo = { backupInfoName, subkey.keyGrip(), QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toUtf8(), "Kleopatra", Formatting::prettyNameAndEMail(subkey.parent()).toUtf8(), }; keydata.insert(keydata.begin(), backupInfo.join(' ') + '\n'); return writeSecretKeyBackup(filename, keydata); } std::vector KeyToCardCommand::Private::readSecretKeyFile() { const auto filename = QString::fromLatin1(subkey.keyGrip()) + QLatin1String{".key"}; const auto path = QDir{Kleo::gnupgPrivateKeysDirectory()}.filePath(filename); QFile file{path}; if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { error(xi18n("Cannot open the private key file %1 for reading.", path)); return {}; } std::vector lines; while (!file.atEnd()) { lines.push_back(file.readLine()); } if (lines.empty()) { error(xi18n("The private key file %1 is empty.", path)); } return lines; } bool KeyToCardCommand::Private::writeSecretKeyBackup(const QString &filename, const std::vector &keydata) { QSaveFile file{filename}; // open the file in binary format because we want to write Unix line endings if (!file.open(QIODevice::WriteOnly)) { error(xi18n("Cannot open the file %1 for writing.", filename)); return false; } for (const auto &line : keydata) { file.write(line); } if (!file.commit()) { error(xi18n("Writing the backup of the secret key to %1 failed.", filename)); return false; }; return true; } void KeyToCardCommand::Private::startDeleteSecretKeyLocally() { const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const auto answer = KMessageBox::questionTwoActions( parentWidgetOrView(), xi18n("Do you really want to delete the local copy of the secret key?"), i18nc("@title:window", "Confirm Deletion"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::Dangerous); if (answer != KMessageBox::ButtonCode::PrimaryAction) { finished(); return; } const auto cmd = QByteArray{"DELETE_KEY --force "} + subkey.keyGrip(); ReaderStatus::mutableInstance()->startSimpleTransaction(card, cmd, q, [this](const GpgME::Error &err) { deleteSecretKeyLocallyFinished(err); }); } void KeyToCardCommand::Private::deleteSecretKeyLocallyFinished(const GpgME::Error &err) { if (err) { - error(xi18nc("@info", "Failed to delete the key:%1", QString::fromUtf8(err.asString()))); + error(xi18nc("@info", "Failed to delete the key:%1", Formatting::errorAsString(err))); } ReaderStatus::mutableInstance()->updateStatus(); success(i18nc("@info", "Successfully copied the key to the card.")); finished(); } KeyToCardCommand::KeyToCardCommand(const GpgME::Subkey &subkey) : CardCommand(new Private(this, subkey)) { } KeyToCardCommand::KeyToCardCommand(const std::string& cardSlot, const std::string &serialNumber, const std::string &appName) : CardCommand(new Private(this, cardSlot, serialNumber, appName)) { } KeyToCardCommand::~KeyToCardCommand() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::~KeyToCardCommand()"; } namespace { bool cardSupportsKeyAlgorithm(const std::shared_ptr &card, const std::string &keyAlgo) { if (card->appName() == OpenPGPCard::AppName) { const auto pgpCard = static_cast(card.get()); const auto cardAlgos = pgpCard->supportedAlgorithms(); return Kleo::any_of(cardAlgos, [keyAlgo](const auto &algo) { return (keyAlgo == algo.id) // || (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpEncKeyRef())) || (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpSigKeyRef())); }); } return false; } } // static std::vector> KeyToCardCommand::getSuitableCards(const GpgME::Subkey &subkey) { std::vector> suitableCards; if (subkey.isNull() || subkey.parent().protocol() != GpgME::OpenPGP) { return suitableCards; } const auto keyAlgo = subkey.algoName(); Kleo::copy_if(ReaderStatus::instance()->getCards(), std::back_inserter(suitableCards), [keyAlgo](const auto &card) { return cardSupportsKeyAlgorithm(card, keyAlgo); }); return suitableCards; } void KeyToCardCommand::Private::keyToCardDone(const GpgME::Error &err) { if (!err && !err.isCanceled()) { updateConnection = connect(ReaderStatus::instance(), &ReaderStatus::updateFinished, q, [this]() { updateDone(); }); ReaderStatus::mutableInstance()->updateCard(serialNumber(), appName); return; } if (err) { error(xi18nc("@info", - "Copying the key to the card failed:%1", QString::fromUtf8(err.asString()))); + "Copying the key to the card failed:%1", Formatting::errorAsString(err))); } finished(); } void KeyToCardCommand::Private::keyToPIVCardDone(const GpgME::Error &err) { - qCDebug(KLEOPATRA_LOG) << q << __func__ << err.asString() << "(" << err.code() << ")"; + qCDebug(KLEOPATRA_LOG) << q << __func__ << Formatting::errorAsString(err) << "(" << err.code() << ")"; #ifdef GPG_ERROR_HAS_NO_AUTH // gpgme 1.13 reports "BAD PIN" instead of "NO AUTH" if (err.code() == GPG_ERR_NO_AUTH || err.code() == GPG_ERR_BAD_PIN) { authenticate(); return; } #endif keyToCardDone(err); } void KeyToCardCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::doStart()"; d->start(); } void KeyToCardCommand::doCancel() { } #undef q_func #undef d_func diff --git a/src/commands/lookupcertificatescommand.cpp b/src/commands/lookupcertificatescommand.cpp index dbe73e000..07674fe92 100644 --- a/src/commands/lookupcertificatescommand.cpp +++ b/src/commands/lookupcertificatescommand.cpp @@ -1,611 +1,611 @@ /* -*- 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 #if QGPGME_SUPPORTS_WKDLOOKUP # include # include #endif #include #include #include #include #include #include #include "kleopatra_debug.h" #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); #if QGPGME_SUPPORTS_WKDLOOKUP void slotWKDLookupResult(const WKDLookupResult &result); #endif 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; } #if QGPGME_SUPPORTS_WKDLOOKUP WKDLookupJob *createWKDLookupJob() const { const auto cbp = QGpgME::openpgp(); return cbp ? cbp->wkdLookupJob() : nullptr; } #endif 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); #if QGPGME_SUPPORTS_WKDLOOKUP void startWKDLookupJob(const QString &str); #endif 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; #if QGPGME_SUPPORTS_WKDLOOKUP QPointer wkdJob; #endif 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); 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); } else { startKeyListJob(OpenPGP, str); #if QGPGME_SUPPORTS_WKDLOOKUP if (str.contains(QLatin1Char{'@'}) && !searchTextToEmailAddress(str).isEmpty()) { startWKDLookupJob(str); } #endif } } } void LookupCertificatesCommand::Private::startKeyListJob(GpgME::Protocol proto, const QString &str) { 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; } } #if QGPGME_SUPPORTS_WKDLOOKUP 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; } } #endif 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(); } #if QGPGME_SUPPORTS_WKDLOOKUP 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(); } #endif 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 #if QGPGME_SUPPORTS_WKDLOOKUP || keyListing.wkdJob #endif ) { // 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", - QString::fromLocal8Bit(result.error().asString()))); + 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) 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/newopenpgpcertificatecommand.cpp b/src/commands/newopenpgpcertificatecommand.cpp index eaac4bb69..f577cfeac 100644 --- a/src/commands/newopenpgpcertificatecommand.cpp +++ b/src/commands/newopenpgpcertificatecommand.cpp @@ -1,263 +1,263 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/newopenpgpcertificatecommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 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 "newopenpgpcertificatecommand.h" #include "command_p.h" #include "dialogs/newopenpgpcertificatedetailsdialog.h" #include "utils/emptypassphraseprovider.h" #include "utils/keyparameters.h" #include "utils/userinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; class NewOpenPGPCertificateCommand::Private : public Command::Private { friend class ::Kleo::NewOpenPGPCertificateCommand; NewOpenPGPCertificateCommand *q_func() const { return static_cast(q); } public: explicit Private(NewOpenPGPCertificateCommand *qq, KeyListController *c) : Command::Private{qq, c} { } void getCertificateDetails(); void createCertificate(); void showResult(const KeyGenerationResult &result); void showErrorDialog(const KeyGenerationResult &result); private: KeyParameters keyParameters; bool protectKeyWithPassword = false; EmptyPassphraseProvider emptyPassphraseProvider; QPointer detailsDialog; QPointer job; QPointer progressDialog; }; NewOpenPGPCertificateCommand::Private *NewOpenPGPCertificateCommand::d_func() { return static_cast(d.get()); } const NewOpenPGPCertificateCommand::Private *NewOpenPGPCertificateCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() void NewOpenPGPCertificateCommand::Private::getCertificateDetails() { detailsDialog = new NewOpenPGPCertificateDetailsDialog; detailsDialog->setAttribute(Qt::WA_DeleteOnClose); applyWindowID(detailsDialog); if (keyParameters.protocol() == KeyParameters::NoProtocol) { const auto settings = Kleo::Settings{}; const KConfigGroup config{KSharedConfig::openConfig(), "CertificateCreationWizard"}; // prefer the last used name and email address over the values retrieved from the system detailsDialog->setName(config.readEntry("NAME", QString{})); if (detailsDialog->name().isEmpty() && settings.prefillName()) { detailsDialog->setName(userFullName()); } detailsDialog->setEmail(config.readEntry("EMAIL", QString{})); if (detailsDialog->email().isEmpty() && settings.prefillEmail()) { detailsDialog->setEmail(userEmailAddress()); } } else { detailsDialog->setKeyParameters(keyParameters); detailsDialog->setProtectKeyWithPassword(protectKeyWithPassword); } connect(detailsDialog, &QDialog::accepted, q, [this]() { keyParameters = detailsDialog->keyParameters(); protectKeyWithPassword = detailsDialog->protectKeyWithPassword(); QMetaObject::invokeMethod( q, [this] { createCertificate(); }, Qt::QueuedConnection); }); connect(detailsDialog, &QDialog::rejected, q, [this]() { canceled(); }); detailsDialog->show(); } void NewOpenPGPCertificateCommand::Private::createCertificate() { Q_ASSERT(keyParameters.protocol() == KeyParameters::OpenPGP); auto keyGenJob = QGpgME::openpgp()->keyGenerationJob(); if (!keyGenJob) { finished(); return; } if (!protectKeyWithPassword) { auto ctx = QGpgME::Job::context(keyGenJob); ctx->setPassphraseProvider(&emptyPassphraseProvider); ctx->setPinentryMode(Context::PinentryLoopback); } connect(keyGenJob, &QGpgME::KeyGenerationJob::result, q, [this](const KeyGenerationResult &result) { QMetaObject::invokeMethod( q, [this, result] { showResult(result); }, Qt::QueuedConnection); }); if (const Error err = keyGenJob->start(keyParameters.toString())) { - error(i18n("Could not start key pair creation: %1", QString::fromUtf8(err.asString()))); + error(i18n("Could not start key pair creation: %1", Formatting::errorAsString(err))); finished(); return; } else { job = keyGenJob; } progressDialog = new QProgressDialog; progressDialog->setAttribute(Qt::WA_DeleteOnClose); applyWindowID(progressDialog); progressDialog->setModal(true); progressDialog->setWindowTitle(i18nc("@title", "Creating Key Pair...")); progressDialog->setLabelText(i18n("The process of creating a key requires large amounts of random numbers. This may require several minutes...")); progressDialog->setRange(0, 0); connect(progressDialog, &QProgressDialog::canceled, job, &QGpgME::Job::slotCancel); connect(job, &QGpgME::Job::done, q, [this]() { if (progressDialog) { progressDialog->accept(); } }); progressDialog->show(); } void NewOpenPGPCertificateCommand::Private::showResult(const KeyGenerationResult &result) { if (result.error().isCanceled()) { finished(); return; } // Ensure that we have the key in the cache Key key; if (!result.error().code() && result.fingerprint()) { std::unique_ptr ctx{Context::createForProtocol(OpenPGP)}; if (ctx) { Error err; key = ctx->key(result.fingerprint(), err, /*secret=*/true); if (!key.isNull()) { KeyCache::mutableInstance()->insert(key); } } } if (!key.isNull()) { success( xi18n("A new OpenPGP certificate was created successfully." "Fingerprint of the new certificate: %1", Formatting::prettyID(key.primaryFingerprint()))); finished(); } else { showErrorDialog(result); } } void NewOpenPGPCertificateCommand::Private::showErrorDialog(const KeyGenerationResult &result) { QString text; if (result.error() || !result.fingerprint()) { text = xi18n( "The creation of a new OpenPGP certificate failed." "Error: %1", - QString::fromLocal8Bit(result.error().asString())); + Formatting::errorAsString(result.error())); } else { // no error and we have a fingerprint, but there was no corresponding key in the key ring text = xi18n( "A new OpenPGP certificate was created successfully, but it has not been found in the key ring." "Fingerprint of the new certificate:%1", Formatting::prettyID(result.fingerprint())); } auto dialog = new QDialog; applyWindowID(dialog); dialog->setWindowTitle(i18nc("@title:window", "Error")); auto buttonBox = new QDialogButtonBox{QDialogButtonBox::Retry | QDialogButtonBox::Ok, dialog}; const auto buttonCode = KMessageBox::createKMessageBox(dialog, buttonBox, QMessageBox::Critical, text, {}, {}, nullptr, {}); if (buttonCode == QDialogButtonBox::Retry) { QMetaObject::invokeMethod( q, [this]() { getCertificateDetails(); }, Qt::QueuedConnection); } else { finished(); } } NewOpenPGPCertificateCommand::NewOpenPGPCertificateCommand() : NewOpenPGPCertificateCommand(nullptr, nullptr) { } NewOpenPGPCertificateCommand::NewOpenPGPCertificateCommand(QAbstractItemView *v, KeyListController *c) : Command(v, new Private(this, c)) { } NewOpenPGPCertificateCommand::~NewOpenPGPCertificateCommand() = default; void NewOpenPGPCertificateCommand::doStart() { d->getCertificateDetails(); } void NewOpenPGPCertificateCommand::doCancel() { if (d->detailsDialog) { d->detailsDialog->close(); } if (d->job) { d->job->slotCancel(); } } #undef d #undef q diff --git a/src/commands/openpgpgeneratecardkeycommand.cpp b/src/commands/openpgpgeneratecardkeycommand.cpp index 6bd756fbf..1c0d0e334 100644 --- a/src/commands/openpgpgeneratecardkeycommand.cpp +++ b/src/commands/openpgpgeneratecardkeycommand.cpp @@ -1,202 +1,204 @@ /* commands/openpgpgeneratecardkeycommand.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 #include "openpgpgeneratecardkeycommand.h" #include "cardcommand_p.h" #include "smartcard/algorithminfo.h" #include "smartcard/openpgpcard.h" #include "smartcard/readerstatus.h" #include "dialogs/gencardkeydialog.h" +#include + #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class OpenPGPGenerateCardKeyCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::OpenPGPGenerateCardKeyCommand; OpenPGPGenerateCardKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(OpenPGPGenerateCardKeyCommand *qq, const std::string &keyref, const std::string &serialNumber, QWidget *p); void init(); private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void ensureDialogCreated(); void generateKey(); private: std::string keyRef; bool overwriteExistingKey = false; std::string algorithm; QPointer dialog; }; OpenPGPGenerateCardKeyCommand::Private *OpenPGPGenerateCardKeyCommand::d_func() { return static_cast(d.get()); } const OpenPGPGenerateCardKeyCommand::Private *OpenPGPGenerateCardKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() OpenPGPGenerateCardKeyCommand::Private::Private(OpenPGPGenerateCardKeyCommand *qq, const std::string &keyRef_, const std::string &serialNumber, QWidget *p) : CardCommand::Private(qq, serialNumber, p) , keyRef{keyRef_} { } void OpenPGPGenerateCardKeyCommand::Private::init() { } void OpenPGPGenerateCardKeyCommand::Private::slotDialogAccepted() { algorithm = dialog->getKeyParams().algorithm; generateKey(); } void OpenPGPGenerateCardKeyCommand::Private::slotDialogRejected() { finished(); } void OpenPGPGenerateCardKeyCommand::Private::slotResult(const GpgME::Error& err) { qCDebug(KLEOPATRA_LOG).nospace() << q << "::Private::" << __func__ << err; if (err) { - error(i18nc("@info", "Generating key failed: %1", QString::fromLatin1(err.asString()))); + error(i18nc("@info", "Generating key failed: %1", Formatting::errorAsString(err))); } else if (!err.isCanceled()) { success(i18nc("@info", "Key successfully generated.")); ReaderStatus::mutableInstance()->updateStatus(); } finished(); } void OpenPGPGenerateCardKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new GenCardKeyDialog(GenCardKeyDialog::KeyAlgorithm, parentWidgetOrView()); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void OpenPGPGenerateCardKeyCommand::Private::generateKey() { qCDebug(KLEOPATRA_LOG).nospace() << q << "::Private::" << __func__; auto pgpCard = ReaderStatus::instance()->getCard(serialNumber()); if (!pgpCard) { error(i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } QByteArrayList command; command << "SCD GENKEY"; if (overwriteExistingKey) { command << "--force"; } if (!algorithm.empty()) { command << "--algo=" + QByteArray::fromStdString(OpenPGPCard::getAlgorithmName(algorithm, keyRef)); } command << "--" << QByteArray::fromStdString(keyRef); ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, command.join(' '), q, [this](const GpgME::Error &err) { slotResult(err); }); } OpenPGPGenerateCardKeyCommand::OpenPGPGenerateCardKeyCommand(const std::string &keyref, const std::string &serialNumber, QWidget *p) : CardCommand(new Private(this, keyref, serialNumber, p)) { d->init(); } OpenPGPGenerateCardKeyCommand::~OpenPGPGenerateCardKeyCommand() { qCDebug(KLEOPATRA_LOG).nospace() << this << "::" << __func__; } void OpenPGPGenerateCardKeyCommand::doStart() { qCDebug(KLEOPATRA_LOG).nospace() << this << "::" << __func__; // check if key exists auto pgpCard = ReaderStatus::instance()->getCard(d->serialNumber()); if (!pgpCard) { d->error(i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(d->serialNumber()))); d->finished(); return; } auto existingKey = pgpCard->keyInfo(d->keyRef).grip; if (!existingKey.empty()) { const QString warningText = i18nc("@info", "

    This card already contains a key in this slot. Continuing will overwrite that key.

    " "

    If there is no backup the existing key will be irrecoverably lost.

    ") + i18n("The existing key has the ID:") + QStringLiteral("
    %1
    ").arg(QString::fromStdString(existingKey)) + (d->keyRef == OpenPGPCard::pgpEncKeyRef() ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString()); const auto choice = KMessageBox::warningContinueCancel(d->parentWidgetOrView(), warningText, i18nc("@title:window", "Overwrite Existing Key"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { d->finished(); return; } d->overwriteExistingKey = true; } d->ensureDialogCreated(); Q_ASSERT(d->dialog); d->dialog->setSupportedAlgorithms(pgpCard->supportedAlgorithms(), "rsa2048"); d->dialog->show(); } void OpenPGPGenerateCardKeyCommand::doCancel() { } #undef d #undef q #include "moc_openpgpgeneratecardkeycommand.cpp" diff --git a/src/commands/pivgeneratecardkeycommand.cpp b/src/commands/pivgeneratecardkeycommand.cpp index a28d5a0c0..c5367bf5f 100644 --- a/src/commands/pivgeneratecardkeycommand.cpp +++ b/src/commands/pivgeneratecardkeycommand.cpp @@ -1,256 +1,258 @@ /* commands/pivgeneratecardkeycommand.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 "pivgeneratecardkeycommand.h" #include "cardcommand_p.h" #include "smartcard/algorithminfo.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "dialogs/gencardkeydialog.h" +#include + #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class PIVGenerateCardKeyCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::PIVGenerateCardKeyCommand; PIVGenerateCardKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(PIVGenerateCardKeyCommand *qq, const std::string &serialNumber, QWidget *p); ~Private() override; void init(); private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void authenticate(); void authenticationFinished(); void authenticationCanceled(); void generateKey(); void ensureDialogCreated(); private: std::string keyRef; bool overwriteExistingKey = false; std::string algorithm; QPointer dialog; bool hasBeenCanceled = false; }; PIVGenerateCardKeyCommand::Private *PIVGenerateCardKeyCommand::d_func() { return static_cast(d.get()); } const PIVGenerateCardKeyCommand::Private *PIVGenerateCardKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() PIVGenerateCardKeyCommand::Private::Private(PIVGenerateCardKeyCommand *qq, const std::string &serialNumber, QWidget *p) : CardCommand::Private(qq, serialNumber, p) , dialog() { } PIVGenerateCardKeyCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::Private::~Private()"; } PIVGenerateCardKeyCommand::PIVGenerateCardKeyCommand(const std::string &serialNumber, QWidget *p) : CardCommand(new Private(this, serialNumber, p)) { d->init(); } void PIVGenerateCardKeyCommand::Private::init() { } PIVGenerateCardKeyCommand::~PIVGenerateCardKeyCommand() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::~PIVGenerateCardKeyCommand()"; } void PIVGenerateCardKeyCommand::setKeyRef(const std::string &keyRef) { d->keyRef = keyRef; } void PIVGenerateCardKeyCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::doStart()"; // check if key exists auto pivCard = ReaderStatus::instance()->getCard(d->serialNumber()); if (!pivCard) { d->error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(d->serialNumber()))); d->finished(); return; } auto existingKey = pivCard->keyInfo(d->keyRef).grip; if (!existingKey.empty()) { const QString warningText = i18nc("@info", "

    This card already contains a key in this slot. Continuing will overwrite that key.

    " "

    If there is no backup the existing key will be irrecoverably lost.

    ") + i18n("The existing key has the ID:") + QStringLiteral("
    %1
    ").arg(QString::fromStdString(existingKey)) + (d->keyRef == PIVCard::keyManagementKeyRef() ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString()); const auto choice = KMessageBox::warningContinueCancel(d->parentWidgetOrView(), warningText, i18nc("@title:window", "Overwrite existing key"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { d->finished(); return; } d->overwriteExistingKey = true; } d->ensureDialogCreated(); Q_ASSERT(d->dialog); d->dialog->show(); } void PIVGenerateCardKeyCommand::doCancel() { } void PIVGenerateCardKeyCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void PIVGenerateCardKeyCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::authenticationFinished()"; if (!hasBeenCanceled) { generateKey(); } } void PIVGenerateCardKeyCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } void PIVGenerateCardKeyCommand::Private::generateKey() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::generateKey()"; auto pivCard = ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } QByteArrayList command; command << "SCD GENKEY"; if (overwriteExistingKey) { command << "--force"; } if (!algorithm.empty()) { command << "--algo=" + QByteArray::fromStdString(algorithm); } command << "--" << QByteArray::fromStdString(keyRef); ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, command.join(' '), q, [this](const GpgME::Error &err) { slotResult(err); }); } void PIVGenerateCardKeyCommand::Private::slotResult(const GpgME::Error& err) { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::slotResult():" - << err.asString() << "(" << err.code() << ")"; + << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err) { #ifdef GPG_ERROR_HAS_NO_AUTH if (err.code() == GPG_ERR_NO_AUTH) { authenticate(); return; } #endif - error(i18nc("@info", "Generating key failed: %1", QString::fromLatin1(err.asString()))); + error(i18nc("@info", "Generating key failed: %1", Formatting::errorAsString(err))); } else if (!err.isCanceled()) { success(i18nc("@info", "Key successfully generated.")); ReaderStatus::mutableInstance()->updateStatus(); } finished(); } void PIVGenerateCardKeyCommand::Private::slotDialogAccepted() { algorithm = dialog->getKeyParams().algorithm; // assume that we are already authenticated to the card generateKey(); } void PIVGenerateCardKeyCommand::Private::slotDialogRejected() { finished(); } void PIVGenerateCardKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new GenCardKeyDialog(GenCardKeyDialog::KeyAlgorithm, parentWidgetOrView()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setSupportedAlgorithms(PIVCard::supportedAlgorithms(keyRef), "rsa2048"); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } #undef d #undef q #include "moc_pivgeneratecardkeycommand.cpp" diff --git a/src/commands/refreshcertificatecommand.cpp b/src/commands/refreshcertificatecommand.cpp index 7d0a1e7d4..c4c728bae 100644 --- a/src/commands/refreshcertificatecommand.cpp +++ b/src/commands/refreshcertificatecommand.cpp @@ -1,303 +1,305 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/refreshcertificatecommand.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 #include "refreshcertificatecommand.h" #include "command_p.h" +#include + #include #include #include #if QGPGME_SUPPORTS_KEY_REFRESH #include #include #endif #include #include "kleopatra_debug.h" using namespace Kleo; using namespace GpgME; class RefreshCertificateCommand::Private : public Command::Private { friend class ::RefreshCertificateCommand; RefreshCertificateCommand *q_func() const { return static_cast(q); } public: explicit Private(RefreshCertificateCommand *qq); ~Private() override; void start(); void cancel(); #if QGPGME_SUPPORTS_KEY_REFRESH std::unique_ptr startOpenPGPJob(); std::unique_ptr startSMIMEJob(); #endif void onOpenPGPJobResult(const ImportResult &result); void onSMIMEJobResult(const Error &err); void showError(const Error &err); private: Key key; #if QGPGME_SUPPORTS_KEY_REFRESH QPointer job; #endif }; RefreshCertificateCommand::Private *RefreshCertificateCommand::d_func() { return static_cast(d.get()); } const RefreshCertificateCommand::Private *RefreshCertificateCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() RefreshCertificateCommand::Private::Private(RefreshCertificateCommand *qq) : Command::Private{qq} { } RefreshCertificateCommand::Private::~Private() = default; namespace { Key getKey(const std::vector &keys) { if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Expected exactly one key, but got" << keys.size(); return {}; } const Key key = keys.front(); if (key.protocol() == GpgME::UnknownProtocol) { qCWarning(KLEOPATRA_LOG) << "Key has unknown protocol"; return {}; } return key; } } void RefreshCertificateCommand::Private::start() { key = getKey(keys()); if (key.isNull()) { finished(); return; } #if QGPGME_SUPPORTS_KEY_REFRESH std::unique_ptr refreshJob; switch (key.protocol()) { case GpgME::OpenPGP: refreshJob = startOpenPGPJob(); break; case GpgME::CMS: refreshJob = startSMIMEJob(); break; default: ; // cannot happen ;-) } if (!refreshJob) { finished(); return; } job = refreshJob.release(); #else - KMessageBox::error(parentWidgetOrView(), i18n("The backend does not support updating individual certificates.")); + error(i18n("The backend does not support updating individual certificates.")); finished(); #endif } void RefreshCertificateCommand::Private::cancel() { #if QGPGME_SUPPORTS_KEY_REFRESH if (job) { job->slotCancel(); } job.clear(); #endif } #if QGPGME_SUPPORTS_KEY_REFRESH std::unique_ptr RefreshCertificateCommand::Private::startOpenPGPJob() { std::unique_ptr refreshJob{QGpgME::openpgp()->receiveKeysJob()}; Q_ASSERT(refreshJob); connect(refreshJob.get(), &QGpgME::ReceiveKeysJob::result, q, [this](const GpgME::ImportResult &result) { onOpenPGPJobResult(result); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(refreshJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(refreshJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif const GpgME::Error err = refreshJob->start({QString::fromLatin1(key.primaryFingerprint())}); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Updating key...")); return refreshJob; } std::unique_ptr RefreshCertificateCommand::Private::startSMIMEJob() { std::unique_ptr refreshJob{QGpgME::smime()->refreshKeysJob()}; Q_ASSERT(refreshJob); connect(refreshJob.get(), &QGpgME::RefreshKeysJob::result, q, [this](const GpgME::Error &err) { onSMIMEJobResult(err); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(refreshJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(refreshJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif const GpgME::Error err = refreshJob->start({key}); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Updating certificate...")); return refreshJob; } #endif namespace { static auto informationOnChanges(const ImportResult &result) { QString text; // if additional keys have been retrieved via WKD, then most of the below // details are just a guess and may concern the additional keys instead of // the refresh keys; this could only be clarified by a thorough comparison of // unrefreshed and refreshed key if (result.numUnchanged() == result.numConsidered()) { // if numUnchanged < numConsidered, then it is not clear whether the refreshed key // hasn't changed or whether another key retrieved via WKD hasn't changed text = i18n("The key hasn't changed."); } else if (result.newRevocations() > 0) { // it is possible that a revoked key has been newly imported via WKD, // but it is much more likely that the refreshed key was revoked text = i18n("The key has been revoked."); } else { // it doesn't make much sense to list below details if the key has been revoked text = i18n("The key has been updated."); QStringList details; if (result.newUserIDs() > 0) { details.push_back(i18n("New user IDs: %1", result.newUserIDs())); } if (result.newSubkeys() > 0) { details.push_back(i18n("New subkeys: %1", result.newSubkeys())); } if (result.newSignatures() > 0) { details.push_back(i18n("New signatures: %1", result.newSignatures())); } if (!details.empty()) { text += QLatin1String{"

    "} + details.join(QLatin1String{"
    "}); } } text = QLatin1String{"

    "} + text + QLatin1String{"

    "}; if (result.numImported() > 0) { text += QLatin1String{"

    "} + i18np("Additionally, one new key has been retrieved.", "Additionally, %1 new keys have been retrieved.", result.numImported()) + QLatin1String{"

    "}; } return text; } } void RefreshCertificateCommand::Private::onOpenPGPJobResult(const ImportResult &result) { if (result.error()) { showError(result.error()); finished(); return; } if (!result.error().isCanceled()) { information(informationOnChanges(result), i18nc("@title:window", "Key Updated")); } finished(); } void RefreshCertificateCommand::Private::onSMIMEJobResult(const Error &err) { if (err) { showError(err); finished(); return; } if (!err.isCanceled()) { information(i18nc("@info", "The certificate has been updated."), i18nc("@title:window", "Certificate Updated")); } finished(); } void RefreshCertificateCommand::Private::showError(const Error &err) { error(xi18nc("@info", "An error occurred while updating the certificate:" "%1", - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), i18nc("@title:window", "Update Failed")); } RefreshCertificateCommand::RefreshCertificateCommand(const GpgME::Key &key) : Command{key, new Private{this}} { } RefreshCertificateCommand::~RefreshCertificateCommand() = default; void RefreshCertificateCommand::doStart() { d->start(); } void RefreshCertificateCommand::doCancel() { d->cancel(); } #undef d #undef q #include "moc_refreshcertificatecommand.cpp" diff --git a/src/commands/reloadkeyscommand.cpp b/src/commands/reloadkeyscommand.cpp index 1151062a9..a7577cc80 100644 --- a/src/commands/reloadkeyscommand.cpp +++ b/src/commands/reloadkeyscommand.cpp @@ -1,99 +1,100 @@ /* -*- mode: c++; c-basic-offset:4 -*- reloadkeyscommand.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 "reloadkeyscommand.h" #include "smartcard/readerstatus.h" #include "command_p.h" +#include #include #include "kleopatra_debug.h" #include using namespace Kleo; using namespace GpgME; class ReloadKeysCommand::Private : public Command::Private { friend class ::Kleo::ReloadKeysCommand; public: Private(ReloadKeysCommand *qq, KeyListController *controller); ~Private() override; void keyListingDone(const KeyListResult &result); }; ReloadKeysCommand::Private *ReloadKeysCommand::d_func() { return static_cast(d.get()); } const ReloadKeysCommand::Private *ReloadKeysCommand::d_func() const { return static_cast(d.get()); } ReloadKeysCommand::ReloadKeysCommand(KeyListController *p) : Command(new Private(this, p)) { } ReloadKeysCommand::ReloadKeysCommand(QAbstractItemView *v, KeyListController *p) : Command(v, new Private(this, p)) { } ReloadKeysCommand::~ReloadKeysCommand() {} ReloadKeysCommand::Private::Private(ReloadKeysCommand *qq, KeyListController *controller) : Command::Private(qq, controller) { } ReloadKeysCommand::Private::~Private() {} void ReloadKeysCommand::Private::keyListingDone(const KeyListResult &result) { if (result.error()) { // ### Show error message here? - qCritical() << "Error occurred during key listing: " << result.error().asString(); + qCritical() << "Error occurred during key listing: " << Formatting::errorAsString(result.error()); } finished(); } #define d d_func() void ReloadKeysCommand::doStart() { const auto view = d->parentWidgetOrView(); if (view && !view->isVisible()) { // Small hack to make redisplay also work nicely when the keylist // is not currently the active widget. SmartCard::ReaderStatus::mutableInstance()->updateStatus(); d->finished(); return; } connect(KeyCache::instance().get(), &KeyCache::keyListingDone, this, [this](const GpgME::KeyListResult &result) { d->keyListingDone(result); }); KeyCache::mutableInstance()->startKeyListing(); } void ReloadKeysCommand::doCancel() { KeyCache::mutableInstance()->cancelKeyListing(); } #undef d #include "moc_reloadkeyscommand.cpp" diff --git a/src/commands/revokecertificationcommand.cpp b/src/commands/revokecertificationcommand.cpp index 2b7ec2698..015fb432b 100644 --- a/src/commands/revokecertificationcommand.cpp +++ b/src/commands/revokecertificationcommand.cpp @@ -1,366 +1,366 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/revokecertificationcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "revokecertificationcommand.h" #include "command_p.h" #include "exportopenpgpcertstoservercommand.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; using namespace QGpgME; namespace { enum class InputType { Key, UserIDs, Certifications, }; struct CertificationData { UserID userId; Key certificationKey; }; static std::vector getCertificationKeys(const GpgME::UserID &userId) { std::vector keys; if (userId.numSignatures() == 0) { qCWarning(KLEOPATRA_LOG) << __func__ << "- Error: Signatures of user ID" << QString::fromUtf8(userId.id()) << "not available"; return keys; } std::vector revokableCertifications; Kleo::copy_if(userId.signatures(), std::back_inserter(revokableCertifications), [](const auto &certification) { return userCanRevokeCertification(certification) == CertificationCanBeRevoked; }); Kleo::transform(revokableCertifications, std::back_inserter(keys), [](const auto &certification) { return KeyCache::instance()->findByKeyIDOrFingerprint(certification.signerKeyID()); }); return keys; } static bool confirmRevocations(QWidget *parent, const std::vector &certifications) { KMessageBox::ButtonCode answer; if (certifications.size() == 1) { const auto [userId, certificationKey] = certifications.front(); const auto message = xi18nc("@info", "You are about to revoke the certification of user ID%1made with the key%2.", QString::fromUtf8(userId.id()), Formatting::formatForComboBox(certificationKey)); answer = KMessageBox::questionTwoActions(parent, message, i18nc("@title:window", "Confirm Revocation"), KGuiItem{i18n("Revoke Certification")}, KStandardGuiItem::cancel()); } else { QStringList l; Kleo::transform(certifications, std::back_inserter(l), [](const auto &c) { return i18n("User ID '%1' certified with key %2", QString::fromUtf8(c.userId.id()), Formatting::formatForComboBox(c.certificationKey)); }); const auto message = i18np("You are about to revoke the following certification:", // "You are about to revoke the following %1 certifications:", certifications.size()); answer = KMessageBox::questionTwoActionsList(parent, message, l, i18nc("@title:window", "Confirm Revocation"), KGuiItem{i18n("Revoke Certifications")}, KStandardGuiItem::cancel()); } return answer == KMessageBox::ButtonCode::PrimaryAction; } } class RevokeCertificationCommand::Private : public Command::Private { friend class ::Kleo::Commands::RevokeCertificationCommand; RevokeCertificationCommand *q_func() const { return static_cast(q); } public: Private(InputType i, RevokeCertificationCommand *qq, KeyListController *c = nullptr); void init(); private: std::vector getCertificationsToRevoke(); void scheduleNextRevocation(); QGpgME::QuickJob *createJob(); void slotResult(const Error &err); private: InputType inputType = InputType::Key; Key certificationTarget; std::vector uids; std::vector certificationsToRevoke; std::vector completedRevocations; QPointer job; }; RevokeCertificationCommand::Private *RevokeCertificationCommand::d_func() { return static_cast(d.get()); } const RevokeCertificationCommand::Private *RevokeCertificationCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() RevokeCertificationCommand::Private::Private(InputType i, RevokeCertificationCommand *qq, KeyListController *c) : Command::Private{qq, c} , inputType{i} { } void RevokeCertificationCommand::Private::init() { const std::vector keys_ = keys(); if (keys_.size() != 1) { qCWarning(KLEOPATRA_LOG) << q << "Expected exactly one key, but got" << keys_.size(); return; } if (keys_.front().protocol() != GpgME::OpenPGP) { qCWarning(KLEOPATRA_LOG) << q << "Expected OpenPGP key, but got" << keys_.front().protocolAsString(); return; } certificationTarget = keys_.front(); } std::vector RevokeCertificationCommand::Private::getCertificationsToRevoke() { if (inputType != InputType::Certifications) { // ensure that the certifications of the key have been loaded if (certificationTarget.userID(0).numSignatures() == 0) { certificationTarget.update(); } // build list of user IDs and revokable certifications const auto userIDsToConsider = (inputType == InputType::Key) ? certificationTarget.userIDs() : uids; for (const auto &userId : userIDsToConsider) { Kleo::transform(getCertificationKeys(userId), std::back_inserter(certificationsToRevoke), [userId](const auto &k) { return CertificationData{userId, k}; }); } } Kleo::erase_if(certificationsToRevoke, [](const auto &c) { return c.certificationKey.isNull(); }); return certificationsToRevoke; } void RevokeCertificationCommand::Private::scheduleNextRevocation() { if (!certificationsToRevoke.empty()) { const auto nextCertification = certificationsToRevoke.back(); job = createJob(); if (!job) { qCWarning(KLEOPATRA_LOG) << q << "Failed to create job"; finished(); return; } job->startRevokeSignature(certificationTarget, nextCertification.certificationKey, {nextCertification.userId}); } else { const auto message = xi18ncp("@info", "The certification has been revoked successfully." "Do you want to publish the revocation?", "%1 certifications have been revoked successfully." "Do you want to publish the revocations?", completedRevocations.size()); const auto yesButton = KGuiItem{i18ncp("@action:button", "Publish Revocation", "Publish Revocations", completedRevocations.size()), QIcon::fromTheme(QStringLiteral("view-certificate-export-server"))}; const auto answer = KMessageBox::questionTwoActions(parentWidgetOrView(), message, i18nc("@title:window", "Confirm Publication"), yesButton, KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::Dangerous); if (answer == KMessageBox::ButtonCode::PrimaryAction) { const auto cmd = new ExportOpenPGPCertsToServerCommand{certificationTarget}; cmd->start(); } finished(); } } void RevokeCertificationCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) { canceled(); return; } if (err) { const auto failedRevocation = certificationsToRevoke.back(); error(xi18nc("@info", "The revocation of the certification of user ID%1made with key%2failed:" "%3", Formatting::prettyNameAndEMail(failedRevocation.userId), Formatting::formatForComboBox(failedRevocation.certificationKey), - QString::fromUtf8(err.asString()))); + Formatting::errorAsString(err))); finished(); return; } completedRevocations.push_back(certificationsToRevoke.back()); certificationsToRevoke.pop_back(); scheduleNextRevocation(); } QGpgME::QuickJob *RevokeCertificationCommand::Private::createJob() { const auto j = QGpgME::openpgp()->quickJob(); if (j) { #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif connect(j, &QGpgME::QuickJob::result, q, [this](const GpgME::Error &error) { slotResult(error); }); } return j; } RevokeCertificationCommand::RevokeCertificationCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{InputType::Key, this, c}} { d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::Key &key) : Command{key, new Private{InputType::Key, this}} { d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::UserID &uid) : Command{uid.parent(), new Private{InputType::UserIDs, this}} { std::vector(1, uid).swap(d->uids); d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const std::vector &uids) : Command{uids.empty() ? Key{} : uids.front().parent(), new Private{InputType::UserIDs, this}} { d->uids = uids; d->init(); } RevokeCertificationCommand::RevokeCertificationCommand(const GpgME::UserID::Signature &signature) : Command{signature.parent().parent(), new Private{InputType::Certifications, this}} { if (!signature.isNull()) { const Key certificationKey = KeyCache::instance()->findByKeyIDOrFingerprint(signature.signerKeyID()); d->certificationsToRevoke = {{signature.parent(), certificationKey}}; } d->init(); } RevokeCertificationCommand::~RevokeCertificationCommand() { qCDebug(KLEOPATRA_LOG) << this << __func__; } // static bool RevokeCertificationCommand::isSupported() { return engineInfo(GpgEngine).engineVersion() >= "2.2.24"; } void RevokeCertificationCommand::doStart() { if (d->certificationTarget.isNull()) { d->finished(); return; } if (!Kleo::all_of(d->uids, userIDBelongsToKey(d->certificationTarget))) { qCWarning(KLEOPATRA_LOG) << this << "User ID <-> Key mismatch!"; d->finished(); return; } const auto certificationsToRevoke = d->getCertificationsToRevoke(); if (certificationsToRevoke.empty()) { switch (d->inputType) { case InputType::Key: d->information(i18n("You cannot revoke any certifications of this key.")); break; case InputType::UserIDs: d->information(i18np("You cannot revoke any certifications of this user ID.", // "You cannot revoke any certifications of these user IDs.", d->uids.size())); break; case InputType::Certifications: d->information(i18n("You cannot revoke this certification.")); break; } d->finished(); return; } if (!confirmRevocations(d->parentWidgetOrView(), certificationsToRevoke)) { d->canceled(); return; } d->scheduleNextRevocation(); } void RevokeCertificationCommand::doCancel() { qCDebug(KLEOPATRA_LOG) << this << __func__; if (d->job) { d->job->slotCancel(); } } #undef d #undef q #include "moc_revokecertificationcommand.cpp" diff --git a/src/commands/revokekeycommand.cpp b/src/commands/revokekeycommand.cpp index 3c3484f8c..6c2593362 100644 --- a/src/commands/revokekeycommand.cpp +++ b/src/commands/revokekeycommand.cpp @@ -1,263 +1,263 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/revokekeycommand.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 #include "revokekeycommand.h" #include "command_p.h" #include "dialogs/revokekeydialog.h" #include #include #if QGPGME_SUPPORTS_KEY_REVOCATION #include #endif #include "kleopatra_debug.h" #include using namespace Kleo; using namespace GpgME; class RevokeKeyCommand::Private : public Command::Private { friend class ::RevokeKeyCommand; RevokeKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(RevokeKeyCommand *qq, KeyListController *c = nullptr); ~Private() override; void start(); void cancel(); private: void ensureDialogCreated(); void onDialogAccepted(); void onDialogRejected(); #if QGPGME_SUPPORTS_KEY_REVOCATION std::unique_ptr startJob(); #endif void onJobResult(const Error &err); void showError(const Error &err); private: Key key; QPointer dialog; #if QGPGME_SUPPORTS_KEY_REVOCATION QPointer job; #endif }; RevokeKeyCommand::Private *RevokeKeyCommand::d_func() { return static_cast(d.get()); } const RevokeKeyCommand::Private *RevokeKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() RevokeKeyCommand::Private::Private(RevokeKeyCommand *qq, KeyListController *c) : Command::Private{qq, c} { } RevokeKeyCommand::Private::~Private() = default; namespace { Key getKey(const std::vector &keys) { if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Expected exactly one key, but got" << keys.size(); return {}; } const Key key = keys.front(); if (key.protocol() != GpgME::OpenPGP) { qCWarning(KLEOPATRA_LOG) << "Expected OpenPGP key, but got" << Formatting::displayName(key.protocol()) << "key"; return {}; } return key; } } void RevokeKeyCommand::Private::start() { key = getKey(keys()); if (key.isNull()) { finished(); return; } if (key.isRevoked()) { information(i18nc("@info", "This key has already been revoked.")); finished(); return; } ensureDialogCreated(); Q_ASSERT(dialog); dialog->setKey(key); dialog->show(); } void RevokeKeyCommand::Private::cancel() { #if QGPGME_SUPPORTS_KEY_REVOCATION if (job) { job->slotCancel(); } job.clear(); #endif } void RevokeKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new RevokeKeyDialog; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, q, [this]() { onDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { onDialogRejected(); }); } void RevokeKeyCommand::Private::onDialogAccepted() { #if QGPGME_SUPPORTS_KEY_REVOCATION auto revokeJob = startJob(); if (!revokeJob) { finished(); return; } job = revokeJob.release(); #endif } void RevokeKeyCommand::Private::onDialogRejected() { canceled(); } namespace { std::vector toStdStrings(const QStringList &l) { std::vector v; v.reserve(l.size()); std::transform(std::begin(l), std::end(l), std::back_inserter(v), std::mem_fn(&QString::toStdString)); return v; } auto descriptionToLines(const QString &description) { std::vector lines; if (!description.isEmpty()) { lines = toStdStrings(description.split(QLatin1Char('\n'))); } return lines; } } #if QGPGME_SUPPORTS_KEY_REVOCATION std::unique_ptr RevokeKeyCommand::Private::startJob() { std::unique_ptr revokeJob{QGpgME::openpgp()->revokeKeyJob()}; Q_ASSERT(revokeJob); connect(revokeJob.get(), &QGpgME::RevokeKeyJob::result, q, [this](const GpgME::Error &err) { onJobResult(err); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(revokeJob.get(), &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(revokeJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif const auto description = descriptionToLines(dialog->description()); const GpgME::Error err = revokeJob->start(key, dialog->reason(), description); if (err) { showError(err); return {}; } Q_EMIT q->info(i18nc("@info:status", "Revoking key...")); return revokeJob; } #endif void RevokeKeyCommand::Private::onJobResult(const Error &err) { if (err) { showError(err); finished(); return; } if (!err.isCanceled()) { information(i18nc("@info", "The key was revoked successfully."), i18nc("@title:window", "Key Revoked")); } finished(); } void RevokeKeyCommand::Private::showError(const Error &err) { error(xi18nc("@info", "An error occurred during the revocation:" "%1", - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), i18nc("@title:window", "Revocation Failed")); } RevokeKeyCommand::RevokeKeyCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{this, c}} { } RevokeKeyCommand::RevokeKeyCommand(const GpgME::Key &key) : Command{key, new Private{this}} { } RevokeKeyCommand::~RevokeKeyCommand() = default; void RevokeKeyCommand::doStart() { d->start(); } void RevokeKeyCommand::doCancel() { d->cancel(); } #undef d #undef q #include "moc_revokekeycommand.cpp" diff --git a/src/commands/revokeuseridcommand.cpp b/src/commands/revokeuseridcommand.cpp index afce4b589..a54e4542d 100644 --- a/src/commands/revokeuseridcommand.cpp +++ b/src/commands/revokeuseridcommand.cpp @@ -1,176 +1,177 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/revokeuseridcommand.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 #include "revokeuseridcommand.h" #include "command_p.h" +#include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; class RevokeUserIDCommand::Private : public Command::Private { friend class ::Kleo::Commands::RevokeUserIDCommand; RevokeUserIDCommand *q_func() const { return static_cast(q); } public: explicit Private(RevokeUserIDCommand *qq, const UserID &userId); ~Private() override; void startJob(); private: void createJob(); void slotResult(const Error &err); void showErrorDialog(const Error &error); void showSuccessDialog(); private: GpgME::UserID userId; QPointer job; }; RevokeUserIDCommand::Private *RevokeUserIDCommand::d_func() { return static_cast(d.get()); } const RevokeUserIDCommand::Private *RevokeUserIDCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() RevokeUserIDCommand::Private::Private(RevokeUserIDCommand *qq, const UserID &userId) : Command::Private{qq} , userId{userId} { } RevokeUserIDCommand::Private::~Private() = default; void Commands::RevokeUserIDCommand::Private::startJob() { createJob(); if (!job) { finished(); return; } const QString uidToRevoke = QString::fromUtf8(engineIsVersion(2, 3, 7) ? userId.uidhash() : userId.id()); job->startRevUid(userId.parent(), uidToRevoke); } void RevokeUserIDCommand::Private::createJob() { Q_ASSERT(!job); const auto backend = QGpgME::openpgp(); if (!backend) { return; } const auto j = backend->quickJob(); if (!j) { return; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif connect(j, &QGpgME::QuickJob::result, q, [this](const GpgME::Error &err) { slotResult(err); }); job = j; } void RevokeUserIDCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) { } else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void RevokeUserIDCommand::Private::showErrorDialog(const Error &err) { error(xi18nc("@info", "An error occurred while trying to revoke the user ID%1." "%2", - QString::fromUtf8(userId.id()), QString::fromLocal8Bit(err.asString())), + QString::fromUtf8(userId.id()), Formatting::errorAsString(err)), i18nc("@title:window", "Revocation Failed")); } void RevokeUserIDCommand::Private::showSuccessDialog() { information(xi18nc("@info", "The user ID%1has been revoked successfully.", QString::fromUtf8(userId.id())), i18nc("@title:window", "Revocation Succeeded")); } RevokeUserIDCommand::RevokeUserIDCommand(const GpgME::UserID &userId) : Command{new Private{this, userId}} { } RevokeUserIDCommand::~RevokeUserIDCommand() { } void RevokeUserIDCommand::doStart() { if (d->userId.isNull()) { d->finished(); return; } const auto key = d->userId.parent(); if (key.protocol() != GpgME::OpenPGP || !key.hasSecret()) { d->finished(); return; } d->startJob(); } void RevokeUserIDCommand::doCancel() { if (d->job) { d->job->slotCancel(); } } #undef d #undef q #include "moc_revokeuseridcommand.cpp" diff --git a/src/commands/setpivcardapplicationadministrationkeycommand.cpp b/src/commands/setpivcardapplicationadministrationkeycommand.cpp index ac1fbd3b9..5805f5cbe 100644 --- a/src/commands/setpivcardapplicationadministrationkeycommand.cpp +++ b/src/commands/setpivcardapplicationadministrationkeycommand.cpp @@ -1,220 +1,222 @@ /* commands/setpivcardapplicationadministrationkeycommand.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 "setpivcardapplicationadministrationkeycommand.h" #include "cardcommand_p.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "dialogs/pivcardapplicationadministrationkeyinputdialog.h" +#include + #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace Kleo::SmartCard; using namespace GpgME; class SetPIVCardApplicationAdministrationKeyCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::SetPIVCardApplicationAdministrationKeyCommand; SetPIVCardApplicationAdministrationKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(SetPIVCardApplicationAdministrationKeyCommand *qq, const std::string &serialNumber, QWidget *p); ~Private() override; void init(); private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void authenticate(); void authenticationFinished(); void authenticationCanceled(); void setAdminKey(); void ensureDialogCreated(); private: QByteArray newAdminKey; QPointer dialog; bool hasBeenCanceled = false; }; SetPIVCardApplicationAdministrationKeyCommand::Private *SetPIVCardApplicationAdministrationKeyCommand::d_func() { return static_cast(d.get()); } const SetPIVCardApplicationAdministrationKeyCommand::Private *SetPIVCardApplicationAdministrationKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() SetPIVCardApplicationAdministrationKeyCommand::Private::Private(SetPIVCardApplicationAdministrationKeyCommand *qq, const std::string &serialNumber, QWidget *p) : CardCommand::Private(qq, serialNumber, p) , dialog() { } SetPIVCardApplicationAdministrationKeyCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::Private::~Private()"; } SetPIVCardApplicationAdministrationKeyCommand::SetPIVCardApplicationAdministrationKeyCommand(const std::string &serialNumber, QWidget *p) : CardCommand(new Private(this, serialNumber, p)) { d->init(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::init() { } SetPIVCardApplicationAdministrationKeyCommand::~SetPIVCardApplicationAdministrationKeyCommand() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::~SetPIVCardApplicationAdministrationKeyCommand()"; } void SetPIVCardApplicationAdministrationKeyCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::doStart()"; d->authenticate(); } void SetPIVCardApplicationAdministrationKeyCommand::doCancel() { } void SetPIVCardApplicationAdministrationKeyCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); cmd->setPrompt(i18n("Please enter the old PIV Card Application Administration Key in hex-encoded form.")); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::authenticationFinished()"; if (!hasBeenCanceled) { setAdminKey(); } } void SetPIVCardApplicationAdministrationKeyCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::setAdminKey() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::setAdminKey()"; ensureDialogCreated(); Q_ASSERT(dialog); dialog->show(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new PIVCardApplicationAdministrationKeyInputDialog(parentWidgetOrView()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setLabelText(newAdminKey.isEmpty() ? i18n("Please enter the new PIV Card Application Administration Key in hex-encoded form. " "The key needs to consist of 24 bytes, i.e. 48 hex-characters.") : i18n("Please enter the new PIV Card Application Administration Key again.")); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void SetPIVCardApplicationAdministrationKeyCommand::Private::slotDialogAccepted() { if (newAdminKey.isEmpty()) { newAdminKey = dialog->adminKey(); dialog = nullptr; setAdminKey(); return; } const QByteArray newAdminKey2 = dialog->adminKey(); if (newAdminKey != newAdminKey2) { error(i18nc("@info", "The two keys you have entered do not match. Please retry.")); newAdminKey.clear(); dialog = nullptr; setAdminKey(); return; } auto pivCard = ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const QByteArray plusPercentEncodedAdminKey = newAdminKey.toPercentEncoding().replace(' ', '+'); const QByteArray command = QByteArray("SCD SETATTR SET-ADM-KEY ") + plusPercentEncodedAdminKey; ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, command, q, [this](const GpgME::Error &err) { slotResult(err); }); } void SetPIVCardApplicationAdministrationKeyCommand::Private::slotDialogRejected() { finished(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::slotResult(const GpgME::Error& err) { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::slotResult():" - << err.asString() << "(" << err.code() << ")"; + << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err) { - error(i18nc("@info", "Setting the PIV Card Application Administration Key failed: %1", QString::fromLatin1(err.asString()))); + error(i18nc("@info", "Setting the PIV Card Application Administration Key failed: %1", Formatting::errorAsString(err))); } else if (!err.isCanceled()) { success(i18nc("@info", "PIV Card Application Administration Key set successfully.")); ReaderStatus::mutableInstance()->updateStatus(); } finished(); } #undef d #undef q #include "moc_setpivcardapplicationadministrationkeycommand.cpp" diff --git a/src/commands/setprimaryuseridcommand.cpp b/src/commands/setprimaryuseridcommand.cpp index 8957c92db..df10a3431 100644 --- a/src/commands/setprimaryuseridcommand.cpp +++ b/src/commands/setprimaryuseridcommand.cpp @@ -1,187 +1,189 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/setprimaryuseridcommand.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 #include "setprimaryuseridcommand.h" #include "command_p.h" +#include + #include #if QGPGME_SUPPORTS_SET_PRIMARY_UID #include #endif #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace GpgME; class SetPrimaryUserIDCommand::Private : public Command::Private { friend class ::Kleo::Commands::SetPrimaryUserIDCommand; SetPrimaryUserIDCommand *q_func() const { return static_cast(q); } public: explicit Private(SetPrimaryUserIDCommand *qq, const UserID &userId); ~Private() override; void startJob(); private: void createJob(); void slotResult(const Error &err); void showErrorDialog(const Error &error); void showSuccessDialog(); private: GpgME::UserID userId; #if QGPGME_SUPPORTS_SET_PRIMARY_UID QPointer job; #endif }; SetPrimaryUserIDCommand::Private *SetPrimaryUserIDCommand::d_func() { return static_cast(d.get()); } const SetPrimaryUserIDCommand::Private *SetPrimaryUserIDCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() SetPrimaryUserIDCommand::Private::Private(SetPrimaryUserIDCommand *qq, const UserID &userId) : Command::Private{qq} , userId{userId} { } SetPrimaryUserIDCommand::Private::~Private() = default; void Commands::SetPrimaryUserIDCommand::Private::startJob() { #if QGPGME_SUPPORTS_SET_PRIMARY_UID createJob(); if (!job) { finished(); return; } job->start(userId); #else error(i18nc("@info", "The backend does not support this operation.")); #endif } void SetPrimaryUserIDCommand::Private::createJob() { #if QGPGME_SUPPORTS_SET_PRIMARY_UID Q_ASSERT(!job); const auto backend = QGpgME::openpgp(); if (!backend) { return; } const auto j = backend->setPrimaryUserIDJob(); if (!j) { return; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif connect(j, &QGpgME::SetPrimaryUserIDJob::result, q, [this](const GpgME::Error &err) { slotResult(err); }); job = j; #endif } void SetPrimaryUserIDCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) { } else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void SetPrimaryUserIDCommand::Private::showErrorDialog(const Error &err) { error(xi18nc("@info", "An error occurred while trying to flag the user ID%1as the primary user ID." "%2", QString::fromUtf8(userId.id()), - QString::fromLocal8Bit(err.asString()))); + Formatting::errorAsString(err))); } void SetPrimaryUserIDCommand::Private::showSuccessDialog() { success(xi18nc("@info", "The user ID%1has been flagged successfully as the primary user ID.", QString::fromUtf8(userId.id()))); } SetPrimaryUserIDCommand::SetPrimaryUserIDCommand(const GpgME::UserID &userId) : Command{new Private{this, userId}} { } SetPrimaryUserIDCommand::~SetPrimaryUserIDCommand() { qCDebug(KLEOPATRA_LOG).nospace() << this << "::" << __func__; } void SetPrimaryUserIDCommand::doStart() { if (d->userId.isNull()) { d->finished(); return; } const auto key = d->userId.parent(); if (key.protocol() != GpgME::OpenPGP || !key.hasSecret()) { d->finished(); return; } d->startJob(); } void SetPrimaryUserIDCommand::doCancel() { qCDebug(KLEOPATRA_LOG).nospace() << this << "::" << __func__; #if QGPGME_SUPPORTS_SET_PRIMARY_UID if (d->job) { d->job->slotCancel(); } #endif } #undef d #undef q diff --git a/src/crypto/decryptverifytask.cpp b/src/crypto/decryptverifytask.cpp index 59e24707c..584cd664f 100644 --- a/src/crypto/decryptverifytask.cpp +++ b/src/crypto/decryptverifytask.cpp @@ -1,1726 +1,1726 @@ /* -*- 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 #if QGPGME_SUPPORTS_ARCHIVE_JOBS #include #endif #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 // 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); } } } 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.", QString::fromLocal8Bit(err.asString()).toHtmlEscaped()); + 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.", QString::fromLocal8Bit(err.asString()).toHtmlEscaped()); + 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(QString::fromLocal8Bit(sig.status().asString())); + 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(QString::fromLocal8Bit(sig.status().asString())); + 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)) { } 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; }; AbstractDecryptVerifyTask::AbstractDecryptVerifyTask(QObject *parent) : Task(parent), d(new Private) {} AbstractDecryptVerifyTask::~AbstractDecryptVerifyTask() {} Mailbox AbstractDecryptVerifyTask::informativeSender() const { return d->informativeSender; } void AbstractDecryptVerifyTask::setInformativeSender(const Mailbox &sender) { d->informativeSender = sender; } class DecryptVerifyTask::Private { DecryptVerifyTask *const q; public: explicit Private(DecryptVerifyTask *qq) : q{qq} { } void startDecryptVerifyJob(); #if QGPGME_SUPPORTS_ARCHIVE_JOBS void startDecryptVerifyArchiveJob(); #endif 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_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()); } 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(); } QString DecryptVerifyTask::outputLabel() const { return d->m_output ? d->m_output->label() : d->m_outputDirectory; } Protocol DecryptVerifyTask::protocol() const { return d->m_protocol; } void DecryptVerifyTask::cancel() { } 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::setOutputDirectory(const QString &directory) { d->m_outputDirectory = directory; } #if QGPGME_SUPPORTS_ARCHIVE_JOBS static bool archiveJobsCanBeUsed(GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::DecryptVerifyArchiveJob::isSupported(); } #endif void DecryptVerifyTask::doStart() { kleo_assert(d->m_backend); #if QGPGME_SUPPORTS_ARCHIVE_JOBS if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) { d->startDecryptVerifyArchiveJob(); } else #endif { 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" << err.asString(); + qCWarning(KLEOPATRA_LOG) << "Failed to set ignore mdc errors" << Formatting::errorAsString(err); } } } } void DecryptVerifyTask::Private::startDecryptVerifyJob() { try { QGpgME::DecryptVerifyJob *const job = m_backend->decryptVerifyJob(); kleo_assert(job); setIgnoreMDCErrorFlag(job, m_ignoreMDCError); QObject::connect(job, &QGpgME::DecryptVerifyJob::result, q, [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult, const QByteArray &plainText) { slotResult(decryptResult, verifyResult, plainText); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job, &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress); #else QObject::connect(job, &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif ensureIOOpen(m_input->ioDevice().get(), m_output->ioDevice().get()); job->start(m_input->ioDevice(), m_output->ioDevice()); } 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())); } } #if QGPGME_SUPPORTS_ARCHIVE_JOBS void DecryptVerifyTask::Private::startDecryptVerifyArchiveJob() { auto *const job = m_backend->decryptVerifyArchiveJob(); kleo_assert(job); setIgnoreMDCErrorFlag(job, m_ignoreMDCError); job->setOutputDirectory(m_outputDirectory); connect(job, &QGpgME::DecryptVerifyArchiveJob::result, q, [this](const GpgME::DecryptionResult &decryptResult, const GpgME::VerificationResult &verifyResult) { slotResult(decryptResult, verifyResult); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job, &QGpgME::Job::jobProgress, q, &DecryptVerifyTask::setProgress); #else connect(job, &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif ensureIOOpen(m_input->ioDevice().get(), nullptr); const auto err = job->start(m_input->ioDevice()); if (err) { q->emitResult(q->fromDecryptVerifyResult(err, {}, {})); } } #endif 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))); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS q->connect(job, &QGpgME::Job::jobProgress, q, &DecryptTask::setProgress); #else q->connect(job, &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif } 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::cancel() { } void DecryptTask::doStart() { kleo_assert(d->m_backend); try { QGpgME::DecryptJob *const job = d->m_backend->decryptJob(); kleo_assert(job); d->registerJob(job); ensureIOOpen(d->m_input->ioDevice().get(), d->m_output->ioDevice().get()); job->start(d->m_input->ioDevice(), d->m_output->ioDevice()); } 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(); #if QGPGME_SUPPORTS_ARCHIVE_JOBS void startDecryptVerifyArchiveJob(); #endif 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_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()); } 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(); } QString VerifyOpaqueTask::outputLabel() const { return d->m_output ? d->m_output->label() : d->m_outputDirectory; } Protocol VerifyOpaqueTask::protocol() const { return d->m_protocol; } void VerifyOpaqueTask::setExtractArchive(bool extract) { d->m_extractArchive = extract; } void VerifyOpaqueTask::setOutputDirectory(const QString &directory) { d->m_outputDirectory = directory; } void VerifyOpaqueTask::cancel() { } void VerifyOpaqueTask::doStart() { kleo_assert(d->m_backend); #if QGPGME_SUPPORTS_ARCHIVE_JOBS if (d->m_extractArchive && archiveJobsCanBeUsed(d->m_protocol)) { d->startDecryptVerifyArchiveJob(); } else #endif { d->startVerifyOpaqueJob(); } } void VerifyOpaqueTask::Private::startVerifyOpaqueJob() { try { QGpgME::VerifyOpaqueJob *const job = m_backend->verifyOpaqueJob(); kleo_assert(job); connect(job, &QGpgME::VerifyOpaqueJob::result, q, [this](const GpgME::VerificationResult &result, const QByteArray &plainText) { slotResult(result, plainText); }); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(job, &QGpgME::Job::jobProgress, q, &VerifyOpaqueTask::setProgress); #else connect(job, &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif 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()); } 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())); } } #if QGPGME_SUPPORTS_ARCHIVE_JOBS void VerifyOpaqueTask::Private::startDecryptVerifyArchiveJob() { auto *const job = m_backend->decryptVerifyArchiveJob(); kleo_assert(job); job->setOutputDirectory(m_outputDirectory); connect(job, &QGpgME::DecryptVerifyArchiveJob::result, q, [this](const DecryptionResult &, const VerificationResult &verifyResult) { slotResult(verifyResult); }); connect(job, &QGpgME::DecryptVerifyArchiveJob::dataProgress, q, &VerifyOpaqueTask::setProgress); ensureIOOpen(m_input->ioDevice().get(), nullptr); const auto err = job->start(m_input->ioDevice()); if (err) { q->emitResult(q->fromVerifyOpaqueResult(err, {}, {})); } } #endif 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))); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS q->connect(job, &QGpgME::Job::jobProgress, q, &VerifyDetachedTask::setProgress); #else q->connect(job, &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif } std::shared_ptr m_input, m_signedData; const QGpgME::Protocol *m_backend = nullptr; Protocol m_protocol = UnknownProtocol; }; 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)); } } 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::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) { 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()); } return i18n("Verifying signature: %1...", d->m_input->label()); } QString VerifyDetachedTask::inputLabel() const { if (d->m_signedData && d->m_input) { 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()); } return d->m_input ? d->m_input->label() : QString(); } QString VerifyDetachedTask::outputLabel() const { return QString(); } Protocol VerifyDetachedTask::protocol() const { return d->m_protocol; } void VerifyDetachedTask::cancel() { } void VerifyDetachedTask::doStart() { kleo_assert(d->m_backend); try { QGpgME::VerifyDetachedJob *const job = d->m_backend->verifyDetachedJob(); kleo_assert(job); d->registerJob(job); ensureIOOpen(d->m_input->ioDevice().get(), nullptr); ensureIOOpen(d->m_signedData->ioDevice().get(), nullptr); job->start(d->m_input->ioDevice(), d->m_signedData->ioDevice()); } 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/encryptemailtask.cpp b/src/crypto/encryptemailtask.cpp index 8abdeac75..dd7ad1822 100644 --- a/src/crypto/encryptemailtask.cpp +++ b/src/crypto/encryptemailtask.cpp @@ -1,237 +1,239 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/encryptemailtask.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 "encryptemailtask.h" #include #include #include #include +#include #include + #include #include #include #include #include #include #include // for Qt::escape using namespace Kleo; using namespace Kleo::Crypto; using namespace GpgME; namespace { class EncryptEMailResult : public Task::Result { const EncryptionResult m_result; const AuditLogEntry m_auditLog; public: EncryptEMailResult(const EncryptionResult &r, const AuditLogEntry &auditLog) : Task::Result(), m_result(r), m_auditLog(auditLog) {} 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; }; QString makeResultString(const EncryptionResult &res) { const Error err = res.error(); if (err.isCanceled()) { return i18n("Encryption canceled."); } if (err) { - return i18n("Encryption failed: %1", QString::fromLocal8Bit(err.asString()).toHtmlEscaped()); + return i18n("Encryption failed: %1", Formatting::errorAsString(err).toHtmlEscaped()); } return i18n("Encryption succeeded."); } } class EncryptEMailTask::Private { friend class ::Kleo::Crypto::EncryptEMailTask; EncryptEMailTask *const q; public: explicit Private(EncryptEMailTask *qq); private: std::unique_ptr createJob(GpgME::Protocol proto); private: void slotResult(const EncryptionResult &); private: std::shared_ptr input; std::shared_ptr output; std::vector recipients; QPointer job; }; EncryptEMailTask::Private::Private(EncryptEMailTask *qq) : q(qq), input(), output(), job(nullptr) { } EncryptEMailTask::EncryptEMailTask(QObject *p) : Task(p), d(new Private(this)) { } EncryptEMailTask::~EncryptEMailTask() {} void EncryptEMailTask::setInput(const std::shared_ptr &input) { kleo_assert(!d->job); kleo_assert(input); d->input = input; } void EncryptEMailTask::setOutput(const std::shared_ptr &output) { kleo_assert(!d->job); kleo_assert(output); d->output = output; } void EncryptEMailTask::setRecipients(const std::vector &recipients) { kleo_assert(!d->job); kleo_assert(!recipients.empty()); d->recipients = recipients; } Protocol EncryptEMailTask::protocol() const { kleo_assert(!d->recipients.empty()); return d->recipients.front().protocol(); } QString EncryptEMailTask::label() const { return d->input ? d->input->label() : QString(); } unsigned long long EncryptEMailTask::inputSize() const { return d->input ? d->input->size() : 0; } void EncryptEMailTask::doStart() { kleo_assert(!d->job); kleo_assert(d->input); kleo_assert(d->output); kleo_assert(!d->recipients.empty()); std::unique_ptr job = d->createJob(protocol()); kleo_assert(job.get()); job->start(d->recipients, d->input->ioDevice(), d->output->ioDevice(), /*alwaysTrust=*/true); d->job = job.release(); } void EncryptEMailTask::cancel() { if (d->job) { d->job->slotCancel(); } } std::unique_ptr EncryptEMailTask::Private::createJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); bool shouldArmor = (proto == OpenPGP || q->asciiArmor()) && !output->binaryOpt(); std::unique_ptr encryptJob(backend->encryptJob(shouldArmor, /*textmode=*/false)); kleo_assert(encryptJob.get()); if (proto == CMS && !q->asciiArmor() && !output->binaryOpt()) { encryptJob->setOutputIsBase64Encoded(true); } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(encryptJob.get(), &QGpgME::Job::jobProgress, q, &EncryptEMailTask::setProgress); #else connect(encryptJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif connect(encryptJob.get(), SIGNAL(result(GpgME::EncryptionResult,QByteArray)), q, SLOT(slotResult(GpgME::EncryptionResult))); return encryptJob; } void EncryptEMailTask::Private::slotResult(const EncryptionResult &result) { const auto *const job = qobject_cast(q->sender()); if (result.error().code()) { output->cancel(); } else { output->finalize(); } q->emitResult(std::shared_ptr(new EncryptEMailResult(result, AuditLogEntry::fromJob(job)))); } QString EncryptEMailResult::overview() const { return makeOverview(makeResultString(m_result)); } QString EncryptEMailResult::details() const { return QString(); } GpgME::Error EncryptEMailResult::error() const { return m_result.error(); } QString EncryptEMailResult::errorString() const { return hasError() ? makeResultString(m_result) : QString(); } AuditLogEntry EncryptEMailResult::auditLog() const { return m_auditLog; } Task::Result::VisualCode EncryptEMailResult::code() const { if (m_result.error().isCanceled()) { return Warning; } return m_result.error().code() ? NeutralError : NeutralSuccess; } #include "moc_encryptemailtask.cpp" diff --git a/src/crypto/gui/certificatelineedit.cpp b/src/crypto/gui/certificatelineedit.cpp index b7392d384..18d49632a 100644 --- a/src/crypto/gui/certificatelineedit.cpp +++ b/src/crypto/gui/certificatelineedit.cpp @@ -1,721 +1,721 @@ /* crypto/gui/certificatelineedit.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "certificatelineedit.h" #include "commands/detailscommand.h" #include "dialogs/groupdetailsdialog.h" #include "utils/accessibility.h" #include "view/errorlabel.h" #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) Q_DECLARE_METATYPE(KeyGroup) static QStringList s_lookedUpKeys; namespace { class CompletionProxyModel : public KeyListSortFilterProxyModel { Q_OBJECT public: CompletionProxyModel(QObject *parent = nullptr) : KeyListSortFilterProxyModel(parent) { } int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent) // pretend that there is only one column to workaround a bug in // QAccessibleTable which provides the accessibility interface for the // completion pop-up return 1; } QVariant data(const QModelIndex &idx, int role) const override { if (!idx.isValid()) { return QVariant(); } switch (role) { case Qt::DecorationRole: { const auto key = KeyListSortFilterProxyModel::data(idx, KeyList::KeyRole).value(); if (!key.isNull()) { return Kleo::Formatting::iconForUid(key.userID(0)); } const auto group = KeyListSortFilterProxyModel::data(idx, KeyList::GroupRole).value(); if (!group.isNull()) { return QIcon::fromTheme(QStringLiteral("group")); } Q_ASSERT(!key.isNull() || !group.isNull()); return QVariant(); } default: return KeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role); } } }; auto createSeparatorAction(QObject *parent) { auto action = new QAction{parent}; action->setSeparator(true); return action; } } // namespace class CertificateLineEdit::Private { CertificateLineEdit *q; public: enum class Status { Empty, //< text is empty Success, //< a certificate or group is set None, //< entered text does not match any certificates or groups Ambiguous, //< entered text matches multiple certificates or groups }; enum class CursorPositioning { MoveToEnd, KeepPosition, MoveToStart, Default = MoveToEnd, }; explicit Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter); QString text() const; void setKey(const GpgME::Key &key); void setGroup(const KeyGroup &group); void setKeyFilter(const std::shared_ptr &filter); void setAccessibleName(const QString &s); private: void updateKey(CursorPositioning positioning); void editChanged(); void editFinished(); void checkLocate(); void onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector &keys); void openDetailsDialog(); void setTextWithBlockedSignals(const QString &s, CursorPositioning positioning); void showContextMenu(const QPoint &pos); QString errorMessage() const; QIcon statusIcon() const; QString statusToolTip() const; void updateStatusAction(); void updateErrorLabel(); void updateAccessibleNameAndDescription(); public: Status mStatus = Status::Empty; bool mEditingInProgress = false; GpgME::Key mKey; KeyGroup mGroup; struct Ui { explicit Ui(QWidget *parent) : lineEdit{parent} , button{parent} , errorLabel{parent} {} QLineEdit lineEdit; QToolButton button; ErrorLabel errorLabel; } ui; private: QString mAccessibleName; KeyListSortFilterProxyModel *const mFilterModel; KeyListSortFilterProxyModel *const mCompleterFilterModel; QCompleter *mCompleter = nullptr; std::shared_ptr mFilter; QAction *const mStatusAction; QAction *const mShowDetailsAction; QPointer mLocateJob; }; CertificateLineEdit::Private::Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter) : q{qq} , ui{qq} , mFilterModel{new KeyListSortFilterProxyModel{qq}} , mCompleterFilterModel{new CompletionProxyModel{qq}} , mCompleter{new QCompleter{qq}} , mFilter{std::shared_ptr{filter}} , mStatusAction{new QAction{qq}} , mShowDetailsAction{new QAction{qq}} { ui.lineEdit.setPlaceholderText(i18n("Please enter a name or email address...")); ui.lineEdit.setClearButtonEnabled(true); ui.lineEdit.setContextMenuPolicy(Qt::CustomContextMenu); ui.lineEdit.addAction(mStatusAction, QLineEdit::LeadingPosition); mCompleterFilterModel->setKeyFilter(mFilter); mCompleterFilterModel->setSourceModel(model); mCompleter->setModel(mCompleterFilterModel); mCompleter->setFilterMode(Qt::MatchContains); mCompleter->setCaseSensitivity(Qt::CaseInsensitive); ui.lineEdit.setCompleter(mCompleter); ui.button.setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new"))); ui.button.setToolTip(i18n("Show certificate list")); ui.button.setAccessibleName(i18n("Show certificate list")); ui.errorLabel.setVisible(false); auto vbox = new QVBoxLayout{q}; vbox->setContentsMargins(0, 0, 0, 0); auto l = new QHBoxLayout; l->setContentsMargins(0, 0, 0, 0); l->addWidget(&ui.lineEdit); l->addWidget(&ui.button); vbox->addLayout(l); vbox->addWidget(&ui.errorLabel); q->setFocusPolicy(ui.lineEdit.focusPolicy()); q->setFocusProxy(&ui.lineEdit); mShowDetailsAction->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); mShowDetailsAction->setText(i18nc("@action:inmenu", "Show Details")); mShowDetailsAction->setEnabled(false); mFilterModel->setSourceModel(model); mFilterModel->setFilterKeyColumn(KeyList::Summary); if (filter) { mFilterModel->setKeyFilter(mFilter); } connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { updateKey(CursorPositioning::KeepPosition); }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupUpdated, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { setTextWithBlockedSignals(Formatting::summaryLine(group), CursorPositioning::KeepPosition); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(CursorPositioning::KeepPosition); }, Qt::QueuedConnection); } }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupRemoved, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { mGroup = KeyGroup(); QSignalBlocker blocky{&ui.lineEdit}; ui.lineEdit.clear(); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(CursorPositioning::KeepPosition); }, Qt::QueuedConnection); } }); connect(&ui.lineEdit, &QLineEdit::editingFinished, q, [this]() { // queue the call of editFinished() to ensure that QCompleter::activated is handled first QMetaObject::invokeMethod(q, [this]() { editFinished(); }, Qt::QueuedConnection); }); connect(&ui.lineEdit, &QLineEdit::textChanged, q, [this]() { editChanged(); }); connect(&ui.lineEdit, &QLineEdit::customContextMenuRequested, q, [this](const QPoint &pos) { showContextMenu(pos); }); connect(mStatusAction, &QAction::triggered, q, [this]() { openDetailsDialog(); }); connect(mShowDetailsAction, &QAction::triggered, q, [this]() { openDetailsDialog(); }); connect(&ui.button, &QToolButton::clicked, q, &CertificateLineEdit::certificateSelectionRequested); connect(mCompleter, qOverload(&QCompleter::activated), q, [this] (const QModelIndex &index) { Key key = mCompleter->completionModel()->data(index, KeyList::KeyRole).value(); auto group = mCompleter->completionModel()->data(index, KeyList::GroupRole).value(); if (!key.isNull()) { q->setKey(key); } else if (!group.isNull()) { q->setGroup(group); } else { qCDebug(KLEOPATRA_LOG) << "Activated item is neither key nor group"; } }); updateKey(CursorPositioning::Default); } void CertificateLineEdit::Private::openDetailsDialog() { if (!q->key().isNull()) { auto cmd = new Commands::DetailsCommand{q->key()}; cmd->setParentWidget(q); cmd->start(); } else if (!q->group().isNull()) { auto dlg = new Dialogs::GroupDetailsDialog{q}; dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setGroup(q->group()); dlg->show(); } } void CertificateLineEdit::Private::setTextWithBlockedSignals(const QString &s, CursorPositioning positioning) { QSignalBlocker blocky{&ui.lineEdit}; const auto cursorPos = ui.lineEdit.cursorPosition(); ui.lineEdit.setText(s); switch(positioning) { case CursorPositioning::KeepPosition: ui.lineEdit.setCursorPosition(cursorPos); break; case CursorPositioning::MoveToStart: ui.lineEdit.setCursorPosition(0); break; case CursorPositioning::MoveToEnd: default: ; // setText() already moved the cursor to the end of the line }; } void CertificateLineEdit::Private::showContextMenu(const QPoint &pos) { if (QMenu *menu = ui.lineEdit.createStandardContextMenu()) { auto *const firstStandardAction = menu->actions().value(0); menu->insertActions(firstStandardAction, {mShowDetailsAction, createSeparatorAction(menu)}); menu->setAttribute(Qt::WA_DeleteOnClose); menu->popup(ui.lineEdit.mapToGlobal(pos)); } } CertificateLineEdit::CertificateLineEdit(AbstractKeyListModel *model, KeyFilter *filter, QWidget *parent) : QWidget{parent} , d{new Private{this, model, filter}} { /* Take ownership of the model to prevent double deletion when the * filter models are deleted */ model->setParent(parent ? parent : this); } CertificateLineEdit::~CertificateLineEdit() = default; void CertificateLineEdit::Private::editChanged() { const bool editingStarted = !mEditingInProgress; mEditingInProgress = true; updateKey(CursorPositioning::Default); if (editingStarted) { Q_EMIT q->editingStarted(); } } void CertificateLineEdit::Private::editFinished() { // perform a first update with the "editing in progress" flag still set updateKey(CursorPositioning::MoveToStart); mEditingInProgress = false; checkLocate(); // perform another update with the "editing in progress" flag cleared // after a key locate may have been started; this makes sure that displaying // an error is delayed until the key locate job has finished updateKey(CursorPositioning::MoveToStart); } void CertificateLineEdit::Private::checkLocate() { if (mStatus != Status::None) { // try to locate key only if text matches no local certificates or groups return; } // Only check once per mailbox const auto mailText = ui.lineEdit.text().trimmed(); if (mailText.isEmpty() || s_lookedUpKeys.contains(mailText)) { return; } s_lookedUpKeys << mailText; if (mLocateJob) { mLocateJob->slotCancel(); mLocateJob.clear(); } auto job = QGpgME::openpgp()->locateKeysJob(); connect(job, &QGpgME::KeyListJob::result, q, [this, job, mailText](const KeyListResult &result, const std::vector &keys) { onLocateJobResult(job, mailText, result, keys); }); if (auto err = job->start({mailText}, /*secretOnly=*/false)) { - qCDebug(KLEOPATRA_LOG) << __func__ << "Error: Starting" << job << "for" << mailText << "failed with" << err.asString(); + qCDebug(KLEOPATRA_LOG) << __func__ << "Error: Starting" << job << "for" << mailText << "failed with" << Formatting::errorAsString(err); } else { mLocateJob = job; qCDebug(KLEOPATRA_LOG) << __func__ << "Started" << job << "for" << mailText; } } void CertificateLineEdit::Private::onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector &keys) { if (mLocateJob != job) { qCDebug(KLEOPATRA_LOG) << __func__ << "Ignoring outdated finished" << job << "for" << email; return; } - qCDebug(KLEOPATRA_LOG) << __func__ << job << "for" << email << "finished with" << result.error().asString() << "and keys" << keys; + qCDebug(KLEOPATRA_LOG) << __func__ << job << "for" << email << "finished with" << Formatting::errorAsString(result.error()) << "and keys" << keys; mLocateJob.clear(); if (!keys.empty() && !keys.front().isNull()) { KeyCache::mutableInstance()->insert(keys.front()); // inserting the key implicitly triggers an update } else { // explicitly trigger an update to display "no key" error updateKey(CursorPositioning::MoveToStart); } } void CertificateLineEdit::Private::updateKey(CursorPositioning positioning) { static const _detail::ByFingerprint keysHaveSameFingerprint; const auto mailText = ui.lineEdit.text().trimmed(); auto newKey = Key(); auto newGroup = KeyGroup(); if (mailText.isEmpty()) { mStatus = Status::Empty; } else { mFilterModel->setFilterRegularExpression(QRegularExpression::escape(mailText)); if (mFilterModel->rowCount() > 1) { // keep current key or group if they still match if (!mKey.isNull()) { for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); Key key = mFilterModel->key(index); if (!key.isNull() && keysHaveSameFingerprint(key, mKey)) { newKey = mKey; break; } } } else if (!mGroup.isNull()) { newGroup = mGroup; for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); KeyGroup group = mFilterModel->group(index); if (!group.isNull() && group.source() == mGroup.source() && group.id() == mGroup.id()) { newGroup = mGroup; break; } } } if (newKey.isNull() && newGroup.isNull()) { mStatus = Status::Ambiguous; } } else if (mFilterModel->rowCount() == 1) { const auto index = mFilterModel->index(0, 0); newKey = mFilterModel->data(index, KeyList::KeyRole).value(); newGroup = mFilterModel->data(index, KeyList::GroupRole).value(); Q_ASSERT(!newKey.isNull() || !newGroup.isNull()); if (newKey.isNull() && newGroup.isNull()) { mStatus = Status::None; } } else { mStatus = Status::None; } } mKey = newKey; mGroup = newGroup; if (!mKey.isNull()) { /* FIXME: This needs to be solved by a multiple UID supporting model */ mStatus = Status::Success; ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions)); if (!mEditingInProgress) { setTextWithBlockedSignals(Formatting::summaryLine(mKey), positioning); } } else if (!mGroup.isNull()) { mStatus = Status::Success; ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions)); if (!mEditingInProgress) { setTextWithBlockedSignals(Formatting::summaryLine(mGroup), positioning); } } else { ui.lineEdit.setToolTip({}); } mShowDetailsAction->setEnabled(mStatus == Status::Success); updateStatusAction(); updateErrorLabel(); Q_EMIT q->keyChanged(); } QString CertificateLineEdit::Private::errorMessage() const { switch (mStatus) { case Status::Empty: case Status::Success: return {}; case Status::None: return i18n("No matching certificates or groups found"); case Status::Ambiguous: return i18n("Multiple matching certificates or groups found"); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } QIcon CertificateLineEdit::Private::statusIcon() const { switch (mStatus) { case Status::Empty: return QIcon::fromTheme(QStringLiteral("emblem-unavailable")); case Status::Success: if (!mKey.isNull()) { return Formatting::iconForUid(mKey.userID(0)); } else if (!mGroup.isNull()) { return Formatting::validityIcon(mGroup); } else { qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; return {}; } case Status::None: case Status::Ambiguous: if (mEditingInProgress || mLocateJob) { return QIcon::fromTheme(QStringLiteral("emblem-question")); } else { return QIcon::fromTheme(QStringLiteral("emblem-error")); } default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } QString CertificateLineEdit::Private::statusToolTip() const { switch (mStatus) { case Status::Empty: return {}; case Status::Success: if (!mKey.isNull()) { return Formatting::validity(mKey.userID(0)); } else if (!mGroup.isNull()) { return Formatting::validity(mGroup); } else { qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; return {}; } case Status::None: case Status::Ambiguous: return errorMessage(); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } void CertificateLineEdit::Private::updateStatusAction() { mStatusAction->setIcon(statusIcon()); mStatusAction->setToolTip(statusToolTip()); } namespace { QString decoratedError(const QString &text) { return text.isEmpty() ? QString() : i18nc("@info", "Error: %1", text); } } void CertificateLineEdit::Private::updateErrorLabel() { const auto currentErrorMessage = ui.errorLabel.text(); const auto newErrorMessage = decoratedError(errorMessage()); if (newErrorMessage == currentErrorMessage) { return; } if (currentErrorMessage.isEmpty() && (mEditingInProgress || mLocateJob)) { // 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.errorLabel.setVisible(!newErrorMessage.isEmpty()); ui.errorLabel.setText(newErrorMessage); updateAccessibleNameAndDescription(); } void CertificateLineEdit::Private::setAccessibleName(const QString &s) { mAccessibleName = s; updateAccessibleNameAndDescription(); } void CertificateLineEdit::Private::updateAccessibleNameAndDescription() { // fall back to default accessible name if accessible name wasn't set explicitly if (mAccessibleName.isEmpty()) { mAccessibleName = getAccessibleName(&ui.lineEdit); } const bool errorShown = ui.errorLabel.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.errorLabel.text() : QString{}; if (ui.lineEdit.accessibleDescription() != description) { ui.lineEdit.setAccessibleDescription(description); } // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute); // screen readers say something like "invalid data" if this state is set; // emulate this by adding "invalid data" to the accessible name of the input field const auto name = errorShown ? mAccessibleName + QLatin1String{", "} + invalidEntryText() : mAccessibleName; if (ui.lineEdit.accessibleName() != name) { ui.lineEdit.setAccessibleName(name); } } Key CertificateLineEdit::key() const { if (isEnabled()) { return d->mKey; } else { return Key(); } } KeyGroup CertificateLineEdit::group() const { if (isEnabled()) { return d->mGroup; } else { return KeyGroup(); } } QString CertificateLineEdit::Private::text() const { return ui.lineEdit.text().trimmed(); } QString CertificateLineEdit::text() const { return d->text(); } void CertificateLineEdit::Private::setKey(const Key &key) { mKey = key; mGroup = KeyGroup(); qCDebug(KLEOPATRA_LOG) << "Setting Key. " << Formatting::summaryLine(key); // position cursor, so that that the start of the summary is visible setTextWithBlockedSignals(Formatting::summaryLine(key), CursorPositioning::MoveToStart); updateKey(CursorPositioning::MoveToStart); } void CertificateLineEdit::setKey(const Key &key) { d->setKey(key); } void CertificateLineEdit::Private::setGroup(const KeyGroup &group) { mGroup = group; mKey = Key(); const QString summary = Formatting::summaryLine(group); qCDebug(KLEOPATRA_LOG) << "Setting KeyGroup. " << summary; // position cursor, so that that the start of the summary is visible setTextWithBlockedSignals(summary, CursorPositioning::MoveToStart); updateKey(CursorPositioning::MoveToStart); } void CertificateLineEdit::setGroup(const KeyGroup &group) { d->setGroup(group); } bool CertificateLineEdit::isEmpty() const { return d->mStatus == Private::Status::Empty; } bool CertificateLineEdit::isEditingInProgress() const { return d->mEditingInProgress; } bool CertificateLineEdit::hasAcceptableInput() const { return d->mStatus == Private::Status::Empty || d->mStatus == Private::Status::Success; } void CertificateLineEdit::Private::setKeyFilter(const std::shared_ptr &filter) { mFilter = filter; mFilterModel->setKeyFilter(filter); mCompleterFilterModel->setKeyFilter(mFilter); updateKey(CursorPositioning::Default); } void CertificateLineEdit::setKeyFilter(const std::shared_ptr &filter) { d->setKeyFilter(filter); } void CertificateLineEdit::setAccessibleNameOfLineEdit(const QString &name) { d->setAccessibleName(name); } #include "certificatelineedit.moc" diff --git a/src/crypto/signemailtask.cpp b/src/crypto/signemailtask.cpp index 05027517b..b05580150 100644 --- a/src/crypto/signemailtask.cpp +++ b/src/crypto/signemailtask.cpp @@ -1,289 +1,290 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/signemailtask.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 "signemailtask.h" #include #include #include #include +#include #include #include #include #include #include #include #include #include // for Qt::escape #include #include using namespace Kleo; using namespace Kleo::Crypto; using namespace GpgME; namespace { class SignEMailResult : public Task::Result { const SigningResult m_result; const AuditLogEntry m_auditLog; public: explicit SignEMailResult(const SigningResult &r, const AuditLogEntry &auditLog) : Task::Result(), m_result(r), m_auditLog(auditLog) {} 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; }; QString makeResultString(const SigningResult &res) { const Error err = res.error(); if (err.isCanceled()) { return i18n("Signing canceled."); } if (err) { - return i18n("Signing failed: %1", QString::fromLocal8Bit(err.asString()).toHtmlEscaped()); + return i18n("Signing failed: %1", Formatting::errorAsString(err).toHtmlEscaped()); } return i18n("Signing succeeded."); } } class SignEMailTask::Private { friend class ::Kleo::Crypto::SignEMailTask; SignEMailTask *const q; public: explicit Private(SignEMailTask *qq); private: std::unique_ptr createJob(GpgME::Protocol proto); private: void slotResult(const SigningResult &); private: std::shared_ptr input; std::shared_ptr output; std::vector signers; bool detached; bool clearsign; QString micAlg; QPointer job; }; SignEMailTask::Private::Private(SignEMailTask *qq) : q(qq), input(), output(), signers(), detached(false), clearsign(false), micAlg(), job(nullptr) { } SignEMailTask::SignEMailTask(QObject *p) : Task(p), d(new Private(this)) { } SignEMailTask::~SignEMailTask() {} void SignEMailTask::setInput(const std::shared_ptr &input) { kleo_assert(!d->job); kleo_assert(input); d->input = input; } void SignEMailTask::setOutput(const std::shared_ptr &output) { kleo_assert(!d->job); kleo_assert(output); d->output = output; } void SignEMailTask::setSigners(const std::vector &signers) { kleo_assert(!d->job); kleo_assert(!signers.empty()); kleo_assert(std::none_of(signers.cbegin(), signers.cend(), std::mem_fn(&Key::isNull))); d->signers = signers; } void SignEMailTask::setDetachedSignature(bool detached) { kleo_assert(!d->job); d->detached = detached; d->clearsign = false; } void SignEMailTask::setClearsign(bool clear) { kleo_assert(!d->job); d->clearsign = clear; d->detached = false; } Protocol SignEMailTask::protocol() const { kleo_assert(!d->signers.empty()); return d->signers.front().protocol(); } QString SignEMailTask::label() const { return d->input ? d->input->label() : QString(); } unsigned long long SignEMailTask::inputSize() const { return d->input ? d->input->size() : 0; } void SignEMailTask::doStart() { kleo_assert(!d->job); kleo_assert(d->input); kleo_assert(d->output); kleo_assert(!d->signers.empty()); d->micAlg.clear(); std::unique_ptr job = d->createJob(protocol()); kleo_assert(job.get()); job->start(d->signers, d->input->ioDevice(), d->output->ioDevice(), d->clearsign ? GpgME::Clearsigned : d->detached ? GpgME::Detached : GpgME::NormalSignatureMode); d->job = job.release(); } void SignEMailTask::cancel() { if (d->job) { d->job->slotCancel(); } } std::unique_ptr SignEMailTask::Private::createJob(GpgME::Protocol proto) { const QGpgME::Protocol *const backend = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); kleo_assert(backend); bool shouldArmor = (proto == OpenPGP || q->asciiArmor()) && !output->binaryOpt(); std::unique_ptr signJob(backend->signJob(/*armor=*/ shouldArmor, /*textmode=*/false)); kleo_assert(signJob.get()); if (proto == CMS && !q->asciiArmor() && !output->binaryOpt()) { signJob->setOutputIsBase64Encoded(true); } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(signJob.get(), &QGpgME::Job::jobProgress, q, &SignEMailTask::setProgress); #else connect(signJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif connect(signJob.get(), SIGNAL(result(GpgME::SigningResult,QByteArray)), q, SLOT(slotResult(GpgME::SigningResult))); return signJob; } static QString collect_micalgs(const GpgME::SigningResult &result, GpgME::Protocol proto) { const std::vector css = result.createdSignatures(); QStringList micalgs; std::transform(css.begin(), css.end(), std::back_inserter(micalgs), [](const GpgME::CreatedSignature &sig) { return QString::fromLatin1(sig.hashAlgorithmAsString()).toLower(); }); if (proto == GpgME::OpenPGP) for (QStringList::iterator it = micalgs.begin(), end = micalgs.end(); it != end; ++it) { it->prepend(QLatin1String("pgp-")); } micalgs.sort(); micalgs.erase(std::unique(micalgs.begin(), micalgs.end()), micalgs.end()); return micalgs.join(QLatin1Char(',')); } void SignEMailTask::Private::slotResult(const SigningResult &result) { const auto *const job = qobject_cast(q->sender()); if (result.error().code()) { output->cancel(); } else { output->finalize(); micAlg = collect_micalgs(result, q->protocol()); } q->emitResult(std::shared_ptr(new SignEMailResult(result, AuditLogEntry::fromJob(job)))); } QString SignEMailTask::micAlg() const { return d->micAlg; } QString SignEMailResult::overview() const { return makeOverview(makeResultString(m_result)); } QString SignEMailResult::details() const { return QString(); } GpgME::Error SignEMailResult::error() const { return m_result.error(); } QString SignEMailResult::errorString() const { return hasError() ? makeResultString(m_result) : QString(); } Task::Result::VisualCode SignEMailResult::code() const { if (m_result.error().isCanceled()) { return Warning; } return m_result.error().code() ? NeutralError : NeutralSuccess; } AuditLogEntry SignEMailResult::auditLog() const { return m_auditLog; } #include "moc_signemailtask.cpp" diff --git a/src/crypto/signencrypttask.cpp b/src/crypto/signencrypttask.cpp index 0d5bbbcf6..f9fa5ff97 100644 --- a/src/crypto/signencrypttask.cpp +++ b/src/crypto/signencrypttask.cpp @@ -1,814 +1,814 @@ /* -*- 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 #if QGPGME_SUPPORTS_ARCHIVE_JOBS #include #include #include #endif #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 QString::fromLocal8Bit(err.asString()).toHtmlEscaped(); + 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 QString::fromLocal8Bit(err.asString()).toHtmlEscaped(); + 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: 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); #if QGPGME_SUPPORTS_ARCHIVE_JOBS 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); #endif 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; 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{nullptr}} { 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, q->label(), output ? output->label() : QString{}, 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; } 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->input) { return d->input->label(); } else if (!d->inputFileNames.empty()) { const auto firstFile = QFileInfo{d->inputFileNames.front()}.fileName(); return d->inputFileNames.size() == 1 ? firstFile : i18nc(", ...", "%1, ...", firstFile); } return {}; } QString SignEncryptTask::tag() const { return Formatting::displayName(protocol()); } unsigned long long SignEncryptTask::inputSize() const { return d->input ? d->input->size() : 0U; } #if QGPGME_SUPPORTS_ARCHIVE_JOBS static bool archiveJobsCanBeUsed(GpgME::Protocol protocol) { return (protocol == GpgME::OpenPGP) && QGpgME::SignEncryptArchiveJob::isSupported(); } #endif void SignEncryptTask::doStart() { kleo_assert(!d->job); if (d->sign) { kleo_assert(!d->signers.empty()); if (d->archive) { kleo_assert(!d->detached && !d->clearsign); } } if (!d->output) { d->output = Output::createFromFile(d->outputFileName, d->m_overwritePolicy); } // release grab on policy, so that output can infer multiple outputs from use count > 1 d->m_overwritePolicy.reset(); const auto proto = protocol(); #if QGPGME_SUPPORTS_ARCHIVE_JOBS if (d->archive && archiveJobsCanBeUsed(proto)) { d->startSignEncryptArchiveJob(proto); } else #endif { d->startSignEncryptJob(proto); } } void SignEncryptTask::Private::startSignEncryptJob(GpgME::Protocol proto) { kleo_assert(input); if (encrypt || symmetric) { Context::EncryptionFlags 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_SUPPORTS_SET_FILENAME if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } #endif job->start(signers, recipients, input->ioDevice(), output->ioDevice(), flags); this->job = job.release(); } else { std::unique_ptr job = createEncryptJob(proto); kleo_assert(job.get()); #if QGPGME_SUPPORTS_SET_FILENAME if (inputFileNames.size() == 1) { job->setFileName(inputFileNames.front()); } #endif job->start(recipients, input->ioDevice(), output->ioDevice(), flags); 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); this->job = job.release(); } else { kleo_assert(!"Either 'sign' or 'encrypt' or 'symmetric' must be set!"); } } void SignEncryptTask::cancel() { 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()); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(signJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); #else connect(signJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif 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()); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(signEncryptJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); #else connect(signEncryptJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif 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()); #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(encryptJob.get(), &QGpgME::Job::jobProgress, q, &SignEncryptTask::setProgress); #else connect(encryptJob.get(), &QGpgME::Job::progress, q, [this](const QString &, int processed, int total) { q->setProgress(processed, total); }); #endif connect(encryptJob.get(), SIGNAL(result(GpgME::EncryptionResult,QByteArray)), q, SLOT(slotResult(GpgME::EncryptionResult))); return encryptJob; } #if QGPGME_SUPPORTS_ARCHIVE_JOBS void SignEncryptTask::Private::startSignEncryptArchiveJob(GpgME::Protocol proto) { kleo_assert(!input); 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::AlwaysTrust; if (symmetric) { flags = static_cast(flags | Context::Symmetric); qCDebug(KLEOPATRA_LOG) << "Adding symmetric flag"; } if (sign) { std::unique_ptr job = createSignEncryptArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); job->start(signers, recipients, relativePaths, output->ioDevice(), flags); this->job = job.release(); } else { std::unique_ptr job = createEncryptArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); job->start(recipients, relativePaths, output->ioDevice(), flags); this->job = job.release(); } } else if (sign) { std::unique_ptr job = createSignArchiveJob(proto); kleo_assert(job.get()); job->setBaseDirectory(baseDirectory); job->start(signers, relativePaths, output->ioDevice()); 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; } #endif 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) { const AuditLogEntry auditLog = AuditLogEntry::fromJob(job); bool outputCreated = false; if (input && input->failed()) { 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()) { output->cancel(); } else { try { kleo_assert(!sresult.isNull() || !eresult.isNull()); 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{q->label(), input ? input->errorString() : QString{}}; const LabelAndError outputInfo{output->label(), output->errorString()}; 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) : /*else*/ 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/certifywidget.cpp b/src/dialogs/certifywidget.cpp index aec6b299e..c06ef999b 100644 --- a/src/dialogs/certifywidget.cpp +++ b/src/dialogs/certifywidget.cpp @@ -1,826 +1,826 @@ /* dialogs/certifywidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2019, 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certifywidget.h" #include #include #include "view/infofield.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 using namespace Kleo; static QDebug operator<<(QDebug s, const GpgME::UserID &userID) { return s << Formatting::prettyUserID(userID); } namespace { // Maybe move this in its own file // based on code from StackOverflow class AnimatedExpander: public QWidget { Q_OBJECT public: explicit AnimatedExpander(const QString &title, const QString &accessibleTitle = {}, QWidget *parent = nullptr); void setContentLayout(QLayout *contentLayout); private: QGridLayout mainLayout; QToolButton toggleButton; QFrame headerLine; QParallelAnimationGroup toggleAnimation; QWidget contentArea; int animationDuration{300}; }; AnimatedExpander::AnimatedExpander(const QString &title, const QString &accessibleTitle, QWidget *parent) : QWidget{parent} { #ifdef Q_OS_WIN // draw dotted focus frame if button has focus; otherwise, draw invisible frame using background color toggleButton.setStyleSheet(QStringLiteral("QToolButton { border: 1px solid palette(window); }" "QToolButton:focus { border: 1px dotted palette(window-text); }")); #else // this works with Breeze style because Breeze draws the focus frame when drawing CE_ToolButtonLabel // while the Windows styles (and Qt's common base style) draw the focus frame before drawing CE_ToolButtonLabel toggleButton.setStyleSheet(QStringLiteral("QToolButton { border: none; }")); #endif toggleButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toggleButton.setArrowType(Qt::ArrowType::RightArrow); toggleButton.setText(title); if (!accessibleTitle.isEmpty()) { toggleButton.setAccessibleName(accessibleTitle); } toggleButton.setCheckable(true); toggleButton.setChecked(false); headerLine.setFrameShape(QFrame::HLine); headerLine.setFrameShadow(QFrame::Sunken); headerLine.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); contentArea.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // start out collapsed contentArea.setMaximumHeight(0); contentArea.setMinimumHeight(0); contentArea.setVisible(false); // let the entire widget grow and shrink with its content toggleAnimation.addAnimation(new QPropertyAnimation(this, "minimumHeight")); toggleAnimation.addAnimation(new QPropertyAnimation(this, "maximumHeight")); toggleAnimation.addAnimation(new QPropertyAnimation(&contentArea, "maximumHeight")); mainLayout.setVerticalSpacing(0); mainLayout.setContentsMargins(0, 0, 0, 0); int row = 0; mainLayout.addWidget(&toggleButton, row, 0, 1, 1, Qt::AlignLeft); mainLayout.addWidget(&headerLine, row++, 2, 1, 1); mainLayout.addWidget(&contentArea, row, 0, 1, 3); setLayout(&mainLayout); QObject::connect(&toggleButton, &QToolButton::clicked, [this](const bool checked) { if (checked) { // make the content visible when expanding starts contentArea.setVisible(true); } toggleButton.setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow); toggleAnimation.setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); toggleAnimation.start(); }); connect(&toggleAnimation, &QAbstractAnimation::finished, [this]() { // hide the content area when it is fully collapsed if (!toggleButton.isChecked()) { contentArea.setVisible(false); } }); } void AnimatedExpander::setContentLayout(QLayout *contentLayout) { delete contentArea.layout(); contentArea.setLayout(contentLayout); const auto collapsedHeight = sizeHint().height() - contentArea.maximumHeight(); auto contentHeight = contentLayout->sizeHint().height(); for (int i = 0; i < toggleAnimation.animationCount() - 1; ++i) { auto expanderAnimation = static_cast(toggleAnimation.animationAt(i)); expanderAnimation->setDuration(animationDuration); expanderAnimation->setStartValue(collapsedHeight); expanderAnimation->setEndValue(collapsedHeight + contentHeight); } auto contentAnimation = static_cast(toggleAnimation.animationAt(toggleAnimation.animationCount() - 1)); contentAnimation->setDuration(animationDuration); contentAnimation->setStartValue(0); contentAnimation->setEndValue(contentHeight); } class SecKeyFilter: public DefaultKeyFilter { public: SecKeyFilter() : DefaultKeyFilter() { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); setCanCertify(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::Set); } bool matches(const GpgME::Key &key, Kleo::KeyFilter::MatchContexts contexts) const override { if (!(availableMatchContexts() & contexts)) { return false; } if (_detail::ByFingerprint()(key, mExcludedKey)) { return false; } return DefaultKeyFilter::matches(key, contexts); } void setExcludedKey(const GpgME::Key &key) { mExcludedKey = key; } private: GpgME::Key mExcludedKey; }; class UserIDItem : public QStandardItem { public: explicit UserIDItem(const GpgME::UserID &uid) : mUserId{uid} {} GpgME::UserID userId() { return mUserId; } private: GpgME::UserID mUserId; }; class UserIDModel : public QStandardItemModel { Q_OBJECT public: enum Role { UserID = Qt::UserRole }; explicit UserIDModel(QObject *parent = nullptr) : QStandardItemModel(parent) {} GpgME::Key certificateToCertify() const { return m_key; } void setKey(const GpgME::Key &key) { m_key = key; clear(); const std::vector ids = key.userIDs(); int i = 0; for (const auto &uid: key.userIDs()) { if (uid.isRevoked() || uid.isInvalid() || Kleo::isRevokedOrExpired(uid)) { // Skip user IDs that cannot really be certified. i++; continue; } const auto item = new UserIDItem{uid}; item->setText(Formatting::prettyUserID(uid)); item->setCheckable(true); item->setEditable(false); item->setCheckState(Qt::Checked); appendRow(item); i++; } } void setCheckedUserIDs(const std::vector &uids) { for (int i = 0, end = rowCount(); i != end; ++i) { const auto uidItem = userIdItem(i); const auto itemUserId = uidItem->userId(); const bool userIdIsInList = Kleo::any_of(uids, [itemUserId](const auto &uid) { return Kleo::userIDsAreEqual(itemUserId, uid); }); uidItem->setCheckState(userIdIsInList ? Qt::Checked : Qt::Unchecked); } } std::vector checkedUserIDs() const { std::vector userIds; userIds.reserve(rowCount()); for (int i = 0; i < rowCount(); ++i) { const auto uidItem = userIdItem(i); if (uidItem->checkState() == Qt::Checked) { userIds.push_back(uidItem->userId()); } } qCDebug(KLEOPATRA_LOG) << "Checked user IDs:" << userIds; return userIds; } private: UserIDItem *userIdItem(int row) const { return static_cast(item(row)); } private: GpgME::Key m_key; }; auto checkBoxSize(const QCheckBox *checkBox) { QStyleOptionButton opt; return checkBox->style()->sizeFromContents(QStyle::CT_CheckBox, &opt, QSize(), checkBox); } auto createInfoButton(const QString &text, QWidget *parent) { auto infoBtn = new QPushButton{parent}; infoBtn->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); infoBtn->setFlat(true); QObject::connect(infoBtn, &QPushButton::clicked, infoBtn, [infoBtn, text] () { const auto pos = infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0); showToolTip(pos, text, infoBtn); }); return infoBtn; } QString dateFormatWithFourDigitYear(QLocale::FormatType format) { // Force the year to be formatted as four digit number, so that // the user can distinguish between 2006 and 2106. return QLocale{}.dateFormat(format). replace(QLatin1String("yy"), QLatin1String("yyyy")). replace(QLatin1String("yyyyyyyy"), QLatin1String("yyyy")); } QString formatDate(const QDate &date, QLocale::FormatType format) { return QLocale{}.toString(date, dateFormatWithFourDigitYear(format)); } class ListView : public QListView { Q_OBJECT public: using QListView::QListView; protected: void focusInEvent(QFocusEvent *event) override { QListView::focusInEvent(event); // queue the invokation, so that it happens after the widget itself got focus QMetaObject::invokeMethod(this, &ListView::forceAccessibleFocusEventForCurrentItem, Qt::QueuedConnection); } private: void forceAccessibleFocusEventForCurrentItem() { // force Qt to send a focus event for the current item to accessibility // tools; otherwise, the user has no idea which item is selected when the // list gets keyboard input focus const auto current = currentIndex(); setCurrentIndex({}); setCurrentIndex(current); } }; } class CertifyWidget::Private { public: Private(CertifyWidget *qq) : q{qq} { auto mainLay = new QVBoxLayout{q}; { auto label = new QLabel{i18n("Verify the fingerprint, mark the user IDs you want to certify, " "and select the key you want to certify the user IDs with.
    " "Note: Only the fingerprint clearly identifies the key and its owner."), q}; label->setWordWrap(true); labelHelper.addLabel(label); mainLay->addWidget(label); } mainLay->addWidget(new KSeparator{Qt::Horizontal, q}); { auto grid = new QGridLayout; grid->setColumnStretch(1, 1); int row = -1; row++; mFprField = std::make_unique(i18n("Fingerprint:"), q); grid->addWidget(mFprField->label(), row, 0); grid->addLayout(mFprField->layout(), row, 1); row++; auto label = new QLabel{i18n("Certify with:"), q}; mSecKeySelect = new KeySelectionCombo{/* secretOnly= */true, q}; mSecKeySelect->setKeyFilter(std::make_shared()); label->setBuddy(mSecKeySelect); grid->addWidget(label, row, 0); grid->addWidget(mSecKeySelect); mainLay->addLayout(grid); } mMissingOwnerTrustInfo = new KMessageWidget{q}; mSetOwnerTrustAction = new QAction{q}; mSetOwnerTrustAction->setText(i18nc("@action:button", "Set Owner Trust")); mSetOwnerTrustAction->setToolTip(i18nc("@info:tooltip", "Click to set the trust level of the selected certification key to ultimate trust. " "This is what you usually want to do for your own keys.")); connect(mSetOwnerTrustAction, &QAction::triggered, q, [this] () { setOwnerTrust(); }); mMissingOwnerTrustInfo->addAction(mSetOwnerTrustAction); mMissingOwnerTrustInfo->setVisible(false); mainLay->addWidget(mMissingOwnerTrustInfo); mainLay->addWidget(new KSeparator{Qt::Horizontal, q}); userIdListView = new ListView{q}; userIdListView->setAccessibleName(i18n("User IDs")); userIdListView->setModel(&mUserIDModel); mainLay->addWidget(userIdListView, 1); // Setup the advanced area auto expander = new AnimatedExpander{i18n("Advanced"), i18n("Show advanced options"), q}; mainLay->addWidget(expander); auto advLay = new QVBoxLayout; mExportCB = new QCheckBox{q}; mExportCB->setText(i18n("Certify for everyone to see (exportable)")); advLay->addWidget(mExportCB); { auto layout = new QHBoxLayout; mPublishCB = new QCheckBox{q}; mPublishCB->setText(i18n("Publish on keyserver afterwards")); mPublishCB->setEnabled(mExportCB->isChecked()); layout->addSpacing(checkBoxSize(mExportCB).width()); layout->addWidget(mPublishCB); advLay->addLayout(layout); } { auto tagsLay = new QHBoxLayout; auto label = new QLabel{i18n("Tags:"), q}; mTagsLE = new QLineEdit{q}; label->setBuddy(mTagsLE); auto infoBtn = createInfoButton(i18n("You can use this to add additional info to a certification.") + QStringLiteral("

    ") + i18n("Tags created by anyone with full certification trust " "are shown in the keylist and can be searched."), q); infoBtn->setAccessibleName(i18n("Explain tags")); tagsLay->addWidget(label); tagsLay->addWidget(mTagsLE, 1); tagsLay->addWidget(infoBtn); advLay->addLayout(tagsLay); } { auto layout = new QHBoxLayout; mExpirationCheckBox = new QCheckBox{q}; mExpirationCheckBox->setText(i18n("Expiration:")); mExpirationDateEdit = new KDateComboBox{q}; mExpirationDateEdit->setOptions(KDateComboBox::EditDate | KDateComboBox::SelectDate | KDateComboBox::DatePicker | KDateComboBox::DateKeywords | KDateComboBox::WarnOnInvalid); static const QDate maxAllowedDate{2106, 2, 5}; const QDate today = QDate::currentDate(); mExpirationDateEdit->setDateRange(today.addDays(1), maxAllowedDate, i18n("The certification must be valid at least until tomorrow."), i18n("The latest allowed certification date is %1.", formatDate(maxAllowedDate, QLocale::ShortFormat))); mExpirationDateEdit->setDateMap({ {today.addYears(2), i18nc("Date for expiration of certification", "Two years from now")}, {today.addYears(1), i18nc("Date for expiration of certification", "One year from now")} }); mExpirationDateEdit->setDate(today.addYears(2)); mExpirationDateEdit->setEnabled(mExpirationCheckBox->isChecked()); auto infoBtn = createInfoButton(i18n("You can use this to set an expiration date for a certification.") + QStringLiteral("

    ") + i18n("By setting an expiration date, you can limit the validity of " "your certification to a certain amount of time. Once the expiration " "date has passed, your certification is no longer valid."), q); infoBtn->setAccessibleName(i18n("Explain expiration")); layout->addWidget(mExpirationCheckBox); layout->addWidget(mExpirationDateEdit, 1); layout->addWidget(infoBtn); advLay->addLayout(layout); } { auto layout = new QHBoxLayout; mTrustSignatureCB = new QCheckBox{q}; mTrustSignatureCB->setText(i18n("Certify as trusted introducer")); auto infoBtn = createInfoButton(i18n("You can use this to certify a trusted introducer for a domain.") + QStringLiteral("

    ") + i18n("All certificates with email addresses belonging to the domain " "that have been certified by the trusted introducer are treated " "as certified, i.e. a trusted introducer acts as a kind of " "intermediate CA for a domain."), q); infoBtn->setAccessibleName(i18n("Explain trusted introducer")); layout->addWidget(mTrustSignatureCB, 1); layout->addWidget(infoBtn); advLay->addLayout(layout); } { auto layout = new QHBoxLayout; auto label = new QLabel{i18n("Domain:"), q}; mTrustSignatureDomainLE = new QLineEdit{q}; mTrustSignatureDomainLE->setEnabled(mTrustSignatureCB->isChecked()); label->setBuddy(mTrustSignatureDomainLE); layout->addSpacing(checkBoxSize(mTrustSignatureCB).width()); layout->addWidget(label); layout->addWidget(mTrustSignatureDomainLE); advLay->addLayout(layout); } expander->setContentLayout(advLay); connect(&mUserIDModel, &QStandardItemModel::itemChanged, q, [this](QStandardItem *item) { onItemChanged(item); }); connect(mExportCB, &QCheckBox::toggled, [this] (bool on) { mPublishCB->setEnabled(on); }); connect(mSecKeySelect, &KeySelectionCombo::currentKeyChanged, [this] (const GpgME::Key &) { updateTags(); checkOwnerTrust(); Q_EMIT q->changed(); }); connect(mExpirationCheckBox, &QCheckBox::toggled, q, [this] (bool checked) { mExpirationDateEdit->setEnabled(checked); Q_EMIT q->changed(); }); connect(mExpirationDateEdit, &KDateComboBox::dateChanged, q, &CertifyWidget::changed); connect(mTrustSignatureCB, &QCheckBox::toggled, q, [this] (bool on) { mTrustSignatureDomainLE->setEnabled(on); Q_EMIT q->changed(); }); connect(mTrustSignatureDomainLE, &QLineEdit::textChanged, q, &CertifyWidget::changed); loadConfig(); } ~Private() = default; void loadConfig() { const KConfigGroup conf(KSharedConfig::openConfig(), "CertifySettings"); mSecKeySelect->setDefaultKey(conf.readEntry("LastKey", QString())); mExportCB->setChecked(conf.readEntry("ExportCheckState", false)); mPublishCB->setChecked(conf.readEntry("PublishCheckState", false)); } void updateTags() { if (mTagsLE->isModified()) { return; } GpgME::Key remarkKey = mSecKeySelect->currentKey(); if (!remarkKey.isNull()) { std::vector uidsWithRemark; QString remark; for (const auto &uid: mTarget.userIDs()) { GpgME::Error err; const char *c_remark = uid.remark(remarkKey, err); if (c_remark) { const QString candidate = QString::fromUtf8(c_remark); if (candidate != remark) { qCDebug(KLEOPATRA_LOG) << "Different remarks on user IDs. Taking last."; remark = candidate; uidsWithRemark.clear(); } uidsWithRemark.push_back(uid); } } // Only select the user IDs with the correct remark if (!remark.isEmpty()) { selectUserIDs(uidsWithRemark); } mTagsLE->setText(remark); } } void updateTrustSignatureDomain() { if (mTrustSignatureDomainLE->text().isEmpty() && mTarget.numUserIDs() == 1) { // try to guess the domain to use for the trust signature const auto address = mTarget.userID(0).addrSpec(); const auto atPos = address.find('@'); if (atPos != std::string::npos) { const auto domain = address.substr(atPos + 1); mTrustSignatureDomainLE->setText(QString::fromUtf8(domain.c_str(), domain.size())); } } } void setTarget(const GpgME::Key &key) { mFprField->setValue(QStringLiteral("") + Formatting::prettyID(key.primaryFingerprint()) + QStringLiteral(""), Formatting::accessibleHexID(key.primaryFingerprint())); mUserIDModel.setKey(key); mTarget = key; auto keyFilter = std::make_shared(); keyFilter->setExcludedKey(mTarget); mSecKeySelect->setKeyFilter(keyFilter); updateTags(); updateTrustSignatureDomain(); } GpgME::Key secKey() const { return mSecKeySelect->currentKey(); } void selectUserIDs(const std::vector &uids) { mUserIDModel.setCheckedUserIDs(uids); } std::vector selectedUserIDs() const { return mUserIDModel.checkedUserIDs(); } bool exportableSelected() const { return mExportCB->isChecked(); } bool publishSelected() const { return mPublishCB->isChecked(); } QString tags() const { return mTagsLE->text().trimmed(); } GpgME::Key target() const { return mTarget; } bool isValid() const { static const QRegularExpression domainNameRegExp{QStringLiteral(R"(^\s*((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}\s*$)"), QRegularExpression::CaseInsensitiveOption}; // do not accept null keys if (mTarget.isNull() || mSecKeySelect->currentKey().isNull()) { return false; } // do not accept empty list of user IDs if (selectedUserIDs().empty()) { return false; } // do not accept if the key to certify is selected as certification key; // this shouldn't happen because the key to certify is excluded from the choice, but better safe than sorry if (_detail::ByFingerprint()(mTarget, mSecKeySelect->currentKey())) { return false; } if (mExpirationCheckBox->isChecked() && !mExpirationDateEdit->isValid()) { return false; } if (mTrustSignatureCB->isChecked() && !domainNameRegExp.match(mTrustSignatureDomainLE->text()).hasMatch()) { return false; } return true; } void checkOwnerTrust() { const auto secretKey = secKey(); if (secretKey.ownerTrust() != GpgME::Key::Ultimate) { mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Information); mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("question"))); mMissingOwnerTrustInfo->setText(i18n("Is this your own key?")); mSetOwnerTrustAction->setEnabled(true); mMissingOwnerTrustInfo->animatedShow(); } else { mMissingOwnerTrustInfo->animatedHide(); } } void setOwnerTrust() { mSetOwnerTrustAction->setEnabled(false); QGpgME::ChangeOwnerTrustJob *const j = QGpgME::openpgp()->changeOwnerTrustJob(); connect(j, &QGpgME::ChangeOwnerTrustJob::result, q, [this] (const GpgME::Error &err) { if (err) { KMessageBox::error(q, i18n("

    Changing the certification trust of the key %1 failed:

    %2

    ", Formatting::formatForComboBox(secKey()), - QString::fromLocal8Bit(err.asString())), + Formatting::errorAsString(err)), i18n("Certification Trust Change Failed")); } if (err || err.isCanceled()) { mSetOwnerTrustAction->setEnabled(true); } else { mMissingOwnerTrustInfo->setMessageType(KMessageWidget::Positive); mMissingOwnerTrustInfo->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); mMissingOwnerTrustInfo->setText(i18n("Owner trust set successfully.")); } }); j->start(secKey(), GpgME::Key::Ultimate); } void onItemChanged(QStandardItem *item) { Q_EMIT q->changed(); #ifndef QT_NO_ACCESSIBILITY if (item) { // assume that the checked state changed QAccessible::State st; st.checked = true; QAccessibleStateChangeEvent e(userIdListView, st); e.setChild(item->index().row()); QAccessible::updateAccessibility(&e); } #endif } public: CertifyWidget *const q; std::unique_ptr mFprField; KeySelectionCombo *mSecKeySelect = nullptr; KMessageWidget *mMissingOwnerTrustInfo = nullptr; ListView *userIdListView = nullptr; QCheckBox *mExportCB = nullptr; QCheckBox *mPublishCB = nullptr; QLineEdit *mTagsLE = nullptr; QCheckBox *mTrustSignatureCB = nullptr; QLineEdit *mTrustSignatureDomainLE = nullptr; QCheckBox *mExpirationCheckBox = nullptr; KDateComboBox *mExpirationDateEdit = nullptr; QAction *mSetOwnerTrustAction = nullptr; LabelHelper labelHelper; UserIDModel mUserIDModel; GpgME::Key mTarget; }; CertifyWidget::CertifyWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } Kleo::CertifyWidget::~CertifyWidget() = default; void CertifyWidget::setTarget(const GpgME::Key &key) { d->setTarget(key); } GpgME::Key CertifyWidget::target() const { return d->target(); } void CertifyWidget::selectUserIDs(const std::vector &uids) { d->selectUserIDs(uids); } std::vector CertifyWidget::selectedUserIDs() const { return d->selectedUserIDs(); } GpgME::Key CertifyWidget::secKey() const { return d->secKey(); } bool CertifyWidget::exportableSelected() const { return d->exportableSelected(); } QString CertifyWidget::tags() const { return d->tags(); } bool CertifyWidget::publishSelected() const { return d->publishSelected(); } bool CertifyWidget::trustSignatureSelected() const { return d->mTrustSignatureCB->isChecked(); } QString CertifyWidget::trustSignatureDomain() const { return d->mTrustSignatureDomainLE->text().trimmed(); } QDate CertifyWidget::expirationDate() const { return d->mExpirationCheckBox->isChecked() ? d->mExpirationDateEdit->date() : QDate{}; } bool CertifyWidget::isValid() const { return d->isValid(); } // For UserID model #include "certifywidget.moc" diff --git a/src/dialogs/exportdialog.cpp b/src/dialogs/exportdialog.cpp index 62cdcb6e0..e164e3f11 100644 --- a/src/dialogs/exportdialog.cpp +++ b/src/dialogs/exportdialog.cpp @@ -1,218 +1,218 @@ /* SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "exportdialog.h" #include "kleopatra_debug.h" #include "view/waitwidget.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; class ExportWidget::Private { public: Private(ExportWidget *qq) : q(qq) {} void setupUi(); GpgME::Key key; GpgME::Subkey subkey; QTextEdit *textEdit; WaitWidget *waitWidget; unsigned int flags; private: ExportWidget *const q; }; void ExportWidget::Private::setupUi() { auto vlay = new QVBoxLayout(q); vlay->setContentsMargins(0, 0, 0, 0); textEdit = new QTextEdit; textEdit->setVisible(false); textEdit->setReadOnly(true); auto fixedFont = QFont(QStringLiteral("Monospace")); fixedFont.setStyleHint(QFont::TypeWriter); textEdit->setFont(fixedFont); textEdit->setReadOnly(true); vlay->addWidget(textEdit); waitWidget = new WaitWidget; waitWidget->setText(i18n("Exporting ...")); vlay->addWidget(waitWidget); } ExportWidget::ExportWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { d->setupUi(); } ExportWidget::~ExportWidget() { } static QString injectComments(const GpgME::Key &key, const QByteArray &data) { QString ret = QString::fromUtf8(data); if (key.protocol() != GpgME::OpenPGP) { return ret; } auto overView = Formatting::toolTip(key, Formatting::Fingerprint | Formatting::UserIDs | Formatting::Issuer | Formatting::Subject | Formatting::ExpiryDates | Formatting::CertificateType | Formatting::CertificateUsage); // Fixup the HTML coming from the toolTip for our own format. overView.remove(QLatin1String("")); overView.replace(QLatin1String(""), QLatin1String("\t")); overView.replace(QLatin1String(""), QLatin1String("\n")); overView.remove(QLatin1String("")); overView.remove(QLatin1String("\n
    ")); overView.replace(QLatin1String("<"), QLatin1String("<")); overView.replace(QLatin1String(">"), QLatin1String(">")); auto overViewLines = overView.split(QLatin1Char('\n')); // Format comments so that they fit for RFC 4880 auto comments = QStringLiteral("Comment: "); comments += overViewLines.join(QLatin1String("\nComment: ")) + QLatin1Char('\n'); ret.insert(37 /* -----BEGIN PGP PUBLIC KEY BLOCK-----\n */, comments); return ret; } void ExportWidget::exportResult(const GpgME::Error &err, const QByteArray &data) { d->waitWidget->setVisible(false); d->textEdit->setVisible(true); if (err) { /* Should not happen. But well,.. */ - d->textEdit->setText(i18nc("%1 is error message", "Failed to export: '%1'", QString::fromLatin1(err.asString()))); + d->textEdit->setText(i18nc("%1 is error message", "Failed to export: '%1'", Formatting::errorAsString(err))); } if (!d->flags) { d->textEdit->setText(injectComments(d->key, data)); } else { d->textEdit->setText(QString::fromUtf8(data)); } } void ExportWidget::setKey(const GpgME::Subkey &key, unsigned int flags) { d->waitWidget->setVisible(true); d->textEdit->setVisible(false); d->key = key.parent(); d->subkey = key; d->flags = flags; auto protocol = d->key.protocol() == GpgME::CMS ? QGpgME::smime() : QGpgME::openpgp(); auto job = protocol->publicKeyExportJob(true); connect(job, &QGpgME::ExportJob::result, this, &ExportWidget::exportResult); job->setExportFlags(flags); job->start(QStringList() << QLatin1String(key.fingerprint()) + QLatin1Char('!')); } void ExportWidget::setKey(const GpgME::Key &key, unsigned int flags) { d->waitWidget->setVisible(true); d->textEdit->setVisible(false); d->key = key; d->flags = flags; auto protocol = key.protocol() == GpgME::CMS ? QGpgME::smime() : QGpgME::openpgp(); auto job = protocol->publicKeyExportJob(true); connect(job, &QGpgME::ExportJob::result, this, &ExportWidget::exportResult); job->setExportFlags(flags); job->start(QStringList() << QLatin1String(key.primaryFingerprint())); } GpgME::Key ExportWidget::key() const { return d->key; } ExportDialog::ExportDialog(QWidget *parent) : QDialog(parent), mWidget(new ExportWidget(this)) { KConfigGroup dialog(KSharedConfig::openStateConfig(), "ExportDialog"); const auto size = dialog.readEntry("Size", QSize(600, 800)); if (size.isValid()) { resize(size); } setWindowTitle(i18nc("@title:window", "Export")); auto l = new QVBoxLayout(this); l->addWidget(mWidget); auto bbox = new QDialogButtonBox(this); auto btn = bbox->addButton(QDialogButtonBox::Close); connect(btn, &QPushButton::pressed, this, &QDialog::accept); l->addWidget(bbox); } ExportDialog::~ExportDialog() { KConfigGroup dialog(KSharedConfig::openStateConfig(), "ExportDialog"); dialog.writeEntry("Size", size()); dialog.sync(); } void ExportDialog::setKey(const GpgME::Key &key, unsigned int flags) { mWidget->setKey(key, flags); } void ExportDialog::setKey(const GpgME::Subkey &key, unsigned int flags) { mWidget->setKey(key, flags); } GpgME::Key ExportDialog::key() const { return mWidget->key(); } diff --git a/src/dialogs/setinitialpindialog.cpp b/src/dialogs/setinitialpindialog.cpp index b9112e768..42d2d3c30 100644 --- a/src/dialogs/setinitialpindialog.cpp +++ b/src/dialogs/setinitialpindialog.cpp @@ -1,200 +1,202 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/setinitialpindialog.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 "setinitialpindialog.h" #include "ui_setinitialpindialog.h" +#include + #include #include -#include +#include #include // for Qt::escape #include using namespace Kleo; using namespace Kleo::Dialogs; using namespace GpgME; enum State { Unknown = 0, NotSet, AlreadySet, Ongoing, Ok, Failed, NumStates }; const char *icons[] = { // PENDING(marc) use better icons, once available "", // Unknown "", // NotSet "security-medium", // AlreadySet "movie-process-working-kde", // Ongoing "security-high", // Ok "security-low", // Failed }; static_assert(sizeof icons / sizeof(*icons) == NumStates, ""); static_assert(sizeof("movie-") == 7, ""); static void update_widget(State state, bool delay, QLabel *resultLB, QLabel *lb, QPushButton *pb, QLabel *statusLB) { Q_ASSERT(state >= 0); Q_ASSERT(state < NumStates); const char *icon = icons[state]; if (qstrncmp(icon, "movie-", sizeof("movie-") - 1) == 0) { resultLB->setMovie(KIconLoader::global()->loadMovie(QLatin1String(icon + sizeof("movie-")), KIconLoader::NoGroup)); } else if (icon && *icon) { resultLB->setPixmap(QIcon::fromTheme(QLatin1String(icon)).pixmap(32)); } else { resultLB->setPixmap(QPixmap()); } lb->setEnabled((state == NotSet || state == Failed) && !delay); pb->setEnabled((state == NotSet || state == Failed) && !delay); if (state == AlreadySet) { statusLB->setText(xi18nc("@info", "No NullPin found. If this PIN was not set by you personally, the card might have been tampered with.")); } } static QString format_error(const Error &err) { if (err.isCanceled()) { return i18nc("@info", "Canceled setting PIN."); } if (err) return xi18nc("@info", "There was an error setting the PIN: %1.", - QString::fromLocal8Bit(err.asString()).toHtmlEscaped()); + Formatting::errorAsString(err).toHtmlEscaped()); else { return i18nc("@info", "PIN set successfully."); } } class SetInitialPinDialog::Private { friend class ::Kleo::Dialogs::SetInitialPinDialog; SetInitialPinDialog *const q; public: explicit Private(SetInitialPinDialog *qq) : q(qq), nksState(Unknown), sigGState(Unknown), ui(q) { } private: void slotNksButtonClicked() { nksState = Ongoing; ui.nksStatusLB->clear(); updateWidgets(); Q_EMIT q->nksPinRequested(); } void slotSigGButtonClicked() { sigGState = Ongoing; ui.sigGStatusLB->clear(); updateWidgets(); Q_EMIT q->sigGPinRequested(); } private: void updateWidgets() { update_widget(nksState, false, ui.nksResultIcon, ui.nksLB, ui.nksPB, ui.nksStatusLB); update_widget(sigGState, nksState == NotSet || nksState == Failed || nksState == Ongoing, ui.sigGResultIcon, ui.sigGLB, ui.sigGPB, ui.sigGStatusLB); ui.closePB()->setEnabled(q->isComplete()); ui.cancelPB()->setEnabled(!q->isComplete()); } private: State nksState, sigGState; struct UI : public Ui::SetInitialPinDialog { explicit UI(Dialogs::SetInitialPinDialog *qq) : Ui::SetInitialPinDialog() { setupUi(qq); closePB()->setEnabled(false); connect(closePB(), &QAbstractButton::clicked, qq, &QDialog::accept); } QAbstractButton *closePB() const { Q_ASSERT(dialogButtonBox); return dialogButtonBox->button(QDialogButtonBox::Close); } QAbstractButton *cancelPB() const { Q_ASSERT(dialogButtonBox); return dialogButtonBox->button(QDialogButtonBox::Cancel); } } ui; }; SetInitialPinDialog::SetInitialPinDialog(QWidget *p) : QDialog(p), d(new Private(this)) { } SetInitialPinDialog::~SetInitialPinDialog() {} void SetInitialPinDialog::setNksPinPresent(bool on) { d->nksState = on ? AlreadySet : NotSet; d->updateWidgets(); } void SetInitialPinDialog::setSigGPinPresent(bool on) { d->sigGState = on ? AlreadySet : NotSet; d->updateWidgets(); } void SetInitialPinDialog::setNksPinSettingResult(const Error &err) { d->ui.nksStatusLB->setText(format_error(err)); d->nksState = err.isCanceled() ? NotSet : err ? Failed : Ok; d->updateWidgets(); } void SetInitialPinDialog::setSigGPinSettingResult(const Error &err) { d->ui.sigGStatusLB->setText(format_error(err)); d->sigGState = err.isCanceled() ? NotSet : err ? Failed : Ok; d->updateWidgets(); } bool SetInitialPinDialog::isComplete() const { return (d->nksState == Ok || d->nksState == AlreadySet); } #include "moc_setinitialpindialog.cpp" diff --git a/src/dialogs/updatenotification.cpp b/src/dialogs/updatenotification.cpp index dfc451330..0f99d639c 100644 --- a/src/dialogs/updatenotification.cpp +++ b/src/dialogs/updatenotification.cpp @@ -1,228 +1,228 @@ /* dialogs/updatenotification.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-License-Identifier: GPL-2.0-or-later */ #include "updatenotification.h" #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; namespace { static void gpgconf_set_update_check(bool value) { auto conf = QGpgME::cryptoConfig(); auto entry = getCryptoConfigEntry(conf, "dirmngr", "allow-version-check"); if (!entry) { qCDebug(KLEOPATRA_LOG) << "allow-version-check entry not found"; return; } if (entry->boolValue() != value) { entry->setBoolValue(value); conf->sync(true); } } } // namespace void UpdateNotification::forceUpdateCheck(QWidget *parent) { auto proc = new QProcess; proc->setProgram(gnupgInstallPath() + QStringLiteral("/gpg-connect-agent.exe")); proc->setArguments({ QStringLiteral("--dirmngr"), QStringLiteral("loadswdb --force"), QStringLiteral("/bye"), }); auto progress = new QProgressDialog(i18n("Searching for updates..."), i18n("Cancel"), 0, 0, parent); progress->setMinimumDuration(0); progress->show(); connect(progress, &QProgressDialog::canceled, [ proc] () { proc->kill(); qCDebug(KLEOPATRA_LOG) << "Update force canceled. Output:" << QString::fromLocal8Bit(proc->readAllStandardOutput()) << "stderr:" << QString::fromLocal8Bit(proc->readAllStandardError()); }); #if QT_DEPRECATED_SINCE(5, 13) connect(proc, qOverload(&QProcess::finished), #else connect(proc, &QProcess::finished, #endif [parent, progress, proc](int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(KLEOPATRA_LOG) << "Update force exited with status:" << exitStatus << "code:" << exitCode; delete progress; proc->deleteLater(); UpdateNotification::checkUpdate(parent, exitStatus == QProcess::NormalExit); }); qCDebug(KLEOPATRA_LOG) << "Starting:" << proc->program() << "args" << proc->arguments(); proc->start(); } void UpdateNotification::checkUpdate(QWidget *parent, bool force) { #ifdef Q_OS_WIN KConfigGroup updatecfg(KSharedConfig::openConfig(), "UpdateNotification"); if (updatecfg.readEntry("NeverShow", false) && !force) { return; } // Gpg defaults to no update check. For Gpg4win we want this // enabled if the user does not explicitly disable update // checks neverShow would be true in that case or // we would have set AllowVersionCheck once and the user // explicitly removed that. if (force || updatecfg.readEntry("AllowVersionCheckSetOnce", false)) { gpgconf_set_update_check (true); updatecfg.writeEntry("AllowVersionCheckSetOnce", true); } const auto current = gpg4winVersionNumber(); GpgME::Error err; const auto lastshown = updatecfg.readEntry("LastShown", QDateTime()); if (!force && lastshown.isValid() && lastshown.addSecs(20 * 60 * 60) > QDateTime::currentDateTime()) { qDebug() << QDateTime::currentDateTime().addSecs(20 * 60 * 60); return; } const auto results = GpgME::SwdbResult::query("gpg4win", current.toUtf8().constData(), &err); if (err) { - qCDebug(KLEOPATRA_LOG) << "update check failed: " << err.asString(); + qCDebug(KLEOPATRA_LOG) << "update check failed: " << Formatting::errorAsString(err); return; } if (results.size() != 1) { /* Should not happen */ qCDebug(KLEOPATRA_LOG) << "more then one result"; return; } const auto result = results[0]; if (result.update()) { const QString newVersion = QStringLiteral("%1.%2.%3").arg(result.version().major) .arg(result.version().minor) .arg(result.version().patch); qCDebug(KLEOPATRA_LOG) << "Have update to version:" << newVersion; UpdateNotification notifier(parent, newVersion); notifier.exec(); updatecfg.writeEntry("LastShown", QDateTime::currentDateTime()); updatecfg.sync(); } else { qCDebug(KLEOPATRA_LOG) << "No update for:" << current; if (force) { KMessageBox::information(parent, i18nc("@info", "No update found in the available version database."), i18nc("@title", "Up to date")); } } #else Q_UNUSED(parent) Q_UNUSED(force) #endif } UpdateNotification::UpdateNotification(QWidget *parent, const QString &version) : QDialog(parent) { resize(400, 200); auto lay = new QGridLayout(this); auto logo = new QLabel; logo->setMaximumWidth(110); setAttribute(Qt::WA_QuitOnClose, false); KIconLoader *const il = KIconLoader::global(); const QString iconPath = il->iconPath(QStringLiteral("gpg4win"), KIconLoader::User); logo->setPixmap(QIcon(iconPath).pixmap(100, 100)); auto label = new HtmlLabel; const QString boldVersion = QStringLiteral("%1").arg(version); label->setHtml(i18nc("%1 is the version number", "Version %1 is available.", boldVersion) + QStringLiteral("

    ") + i18nc("Link to NEWS style changelog", "See the new features.")); label->setOpenExternalLinks(true); label->setTextInteractionFlags(Qt::TextBrowserInteraction); label->setWordWrap(true); setWindowTitle(i18nc("@title:window", "Update Available")); setWindowIcon(QIcon(QLatin1String("gpg4win"))); lay->addWidget(logo, 0, 0); lay->addWidget(label, 0, 1); const auto chk = new QCheckBox (i18n("Show this notification for future updates.")); lay->addWidget(chk, 1, 0, 1, -1); KConfigGroup updatecfg(KSharedConfig::openConfig(), "UpdateNotification"); chk->setChecked(!updatecfg.readEntry("NeverShow", false)); const auto bb = new QDialogButtonBox(); const auto b = bb->addButton(i18n("&Get update"), QDialogButtonBox::AcceptRole); b->setDefault(true); b->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down"))); bb->addButton(QDialogButtonBox::Cancel); lay->addWidget(bb, 2, 0, 1, -1); connect (bb, &QDialogButtonBox::accepted, this, [this, chk]() { QDesktopServices::openUrl(QUrl(QStringLiteral("https://www.gpg4win.org/download.html"))); KConfigGroup updatecfg(KSharedConfig::openConfig(), "UpdateNotification"); updatecfg.writeEntry("NeverShow", !chk->isChecked()); gpgconf_set_update_check (chk->isChecked()); QDialog::accept(); }); connect (bb, &QDialogButtonBox::rejected, this, [this, chk]() { KConfigGroup updatecfg(KSharedConfig::openConfig(), "UpdateNotification"); updatecfg.writeEntry("NeverShow", !chk->isChecked()); gpgconf_set_update_check (chk->isChecked()); QDialog::reject(); }); } diff --git a/src/dialogs/weboftrustwidget.cpp b/src/dialogs/weboftrustwidget.cpp index 6ed78c243..fee9374f8 100644 --- a/src/dialogs/weboftrustwidget.cpp +++ b/src/dialogs/weboftrustwidget.cpp @@ -1,347 +1,348 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "weboftrustwidget.h" #include "commands/certifycertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "utils/keys.h" #include "utils/tags.h" +#include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; class WebOfTrustWidget::Private { friend class ::Kleo::WebOfTrustWidget; WebOfTrustWidget *const q; private: GpgME::Key key; UserIDListModel certificationsModel; QGpgME::KeyListJob *keyListJob = nullptr; NavigatableTreeView *certificationsTV = nullptr; QAction *detailsAction = nullptr; QAction *certifyAction = nullptr; QAction *revokeAction = nullptr; public: Private(WebOfTrustWidget *qq) : q{qq} { certificationsModel.enableRemarks(Tags::tagsEnabled()); certificationsTV = new NavigatableTreeView{q}; certificationsTV->setAccessibleName(i18n("User IDs and certifications")); certificationsTV->setModel(&certificationsModel); certificationsTV->setAllColumnsShowFocus(false); certificationsTV->setSelectionMode(QAbstractItemView::SingleSelection); if (!Tags::tagsEnabled()) { certificationsTV->hideColumn(static_cast(UserIDListModel::Column::Tags)); } auto vLay = new QVBoxLayout(q); vLay->setContentsMargins(0, 0, 0, 0); vLay->addWidget(certificationsTV); detailsAction = new QAction{QIcon::fromTheme(QStringLiteral("dialog-information")), i18nc("@action", "Show Certificate Details"), q}; connect(detailsAction, &QAction::triggered, q, [this]() { showCertificateDetails(); }); certifyAction = new QAction{QIcon::fromTheme(QStringLiteral("view-certificate-sign")), i18nc("@action", "Add Certification"), q}; connect(certifyAction, &QAction::triggered, q, [this]() { addCertification(); }); if (Kleo::Commands::RevokeCertificationCommand::isSupported()) { revokeAction = new QAction{QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), i18nc("@action", "Revoke Certification"), q}; connect(revokeAction, &QAction::triggered, q, [this]() { revokeCertification(); }); } connect(certificationsTV, &QAbstractItemView::doubleClicked, q, [this]() { certificationDblClicked(); }); certificationsTV->setContextMenuPolicy(Qt::CustomContextMenu); connect(certificationsTV, &QWidget::customContextMenuRequested, q, [this] (const QPoint &p) { contextMenuRequested(p); }); connect(certificationsTV->selectionModel(), &QItemSelectionModel::currentRowChanged, q, [this]() { updateActions(); }); updateActions(); } GpgME::UserID selectedUserID() { return certificationsModel.userID(certificationsTV->currentIndex()); } GpgME::UserID::Signature selectedCertification() { return certificationsModel.signature(certificationsTV->currentIndex()); } void certificationDblClicked() { showCertificateDetails(); } void showCertificateDetails() { const auto signature = selectedCertification(); if (signature.isNull()) { qCDebug(KLEOPATRA_LOG) << __func__ << "- no certification selected"; return; } auto cmd = Command::commandForQuery(QString::fromUtf8(signature.signerKeyID())); cmd->setParentWId(q->winId()); cmd->start(); } void addCertification() { auto userID = selectedUserID(); if (userID.isNull()) { userID = selectedCertification().parent(); } if (userID.isNull()) { qCDebug(KLEOPATRA_LOG) << __func__ << "- no user ID or certification selected"; return; } auto cmd = new Kleo::Commands::CertifyCertificateCommand(userID); cmd->setParentWidget(q); certificationsTV->setEnabled(false); connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this]() { certificationsTV->setEnabled(true); // Trigger an update when done q->setKey(key); }); cmd->start(); } void revokeCertification() { Command *cmd = nullptr; if (const auto signature = selectedCertification(); !signature.isNull()) { cmd = new Kleo::Commands::RevokeCertificationCommand(signature); } else if (const auto userID = selectedUserID(); !userID.isNull()) { cmd = new Kleo::Commands::RevokeCertificationCommand(userID); } else { qCDebug(KLEOPATRA_LOG) << __func__ << "- no user ID or certification selected"; return; } cmd->setParentWidget(q); certificationsTV->setEnabled(false); connect(cmd, &Kleo::Commands::RevokeCertificationCommand::finished, q, [this]() { certificationsTV->setEnabled(true); // Trigger an update when done q->setKey(key); }); cmd->start(); } void addActionsForUserID(QMenu *menu) { menu->addAction(certifyAction); if (revokeAction) { menu->addAction(revokeAction); } } void addActionsForSignature(QMenu *menu) { menu->addAction(detailsAction); menu->addAction(certifyAction); if (revokeAction) { menu->addAction(revokeAction); if (!revokeAction->isEnabled()) { menu->setToolTipsVisible(true); } } } void updateActions() { const auto userCanSignUserIDs = userHasCertificationKey(); const auto keyCanBeCertified = Kleo::canBeCertified(key); const auto userID = selectedUserID(); const auto signature = selectedCertification(); detailsAction->setEnabled(!signature.isNull()); certifyAction->setEnabled(keyCanBeCertified && userCanSignUserIDs && (!userID.isNull() || !signature.isNull())); if (revokeAction) { revokeAction->setToolTip({}); if (!signature.isNull()) { const auto revocationFeasibility = userCanRevokeCertification(signature); revokeAction->setEnabled(revocationFeasibility == CertificationCanBeRevoked); switch (revocationFeasibility) { case CertificationCanBeRevoked: break; case CertificationNotMadeWithOwnKey: revokeAction->setToolTip(i18n("You cannot revoke this certification because it wasn't made with one of your keys (or the required secret key is missing).")); break; case CertificationIsSelfSignature: revokeAction->setToolTip(i18n("Revocation of self-certifications is currently not possible.")); break; case CertificationIsRevocation: revokeAction->setToolTip(i18n("You cannot revoke this revocation certification. (But you can re-certify the corresponding user ID.)")); break; case CertificationIsExpired: revokeAction->setToolTip(i18n("You cannot revoke this expired certification.")); break; case CertificationIsInvalid: revokeAction->setToolTip(i18n("You cannot revoke this invalid certification.")); break; case CertificationKeyNotAvailable: revokeAction->setToolTip(i18n("You cannot revoke this certification because the required secret key is not available.")); break; }; } else if (!userID.isNull()) { const bool canRevokeCertification = userCanRevokeCertifications(userID); revokeAction->setEnabled(canRevokeCertification); if (!canRevokeCertification) { revokeAction->setToolTip(i18n("You cannot revoke any of the certifications of this user ID. Select any of the certifications for details.")); } } else { revokeAction->setEnabled(false); } } } void contextMenuRequested(const QPoint &p) { const auto index = certificationsTV->indexAt(p); const auto userID = certificationsModel.userID(index); const auto signature = certificationsModel.signature(index); if (userID.isNull() && signature.isNull()) { return; } auto menu = new QMenu(q); if (!userID.isNull()) { addActionsForUserID(menu); } else if (!signature.isNull()) { addActionsForSignature(menu); } connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); menu->popup(certificationsTV->viewport()->mapToGlobal(p)); } void startSignatureListing() { if (keyListJob) { return; } QGpgME::KeyListJob *const job = QGpgME::openpgp()->keyListJob(/*remote*/false, /*includeSigs*/true, /*validate*/true); if (!job) { return; } if (Tags::tagsEnabled()) { job->addMode(GpgME::SignatureNotations); } connect(job, &QGpgME::KeyListJob::result, q, &WebOfTrustWidget::signatureListingDone); connect(job, &QGpgME::KeyListJob::nextKey, q, &WebOfTrustWidget::signatureListingNextKey); job->start(QStringList(QString::fromLatin1(key.primaryFingerprint()))); keyListJob = job; } }; WebOfTrustWidget::WebOfTrustWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } WebOfTrustWidget::~WebOfTrustWidget() = default; QAction *WebOfTrustWidget::detailsAction() const { return d->detailsAction; } QAction *WebOfTrustWidget::certifyAction() const { return d->certifyAction; } QAction *WebOfTrustWidget::revokeAction() const { return d->revokeAction; } GpgME::Key WebOfTrustWidget::key() const { return d->key; } void WebOfTrustWidget::setKey(const GpgME::Key &key) { if (key.protocol() != GpgME::OpenPGP) { qCDebug(KLEOPATRA_LOG) << "List of Certifications is only supported for OpenPGP keys"; return; } d->key = key; d->certificationsModel.setKey(key); d->updateActions(); d->certificationsTV->expandAll(); d->certificationsTV->header()->resizeSections(QHeaderView::ResizeToContents); d->startSignatureListing(); } void WebOfTrustWidget::signatureListingNextKey(const GpgME::Key &key) { GpgME::Key merged = key; merged.mergeWith(d->key); setKey(merged); } void WebOfTrustWidget::signatureListingDone(const GpgME::KeyListResult &result) { if (result.error()) { KMessageBox::information(this, xi18nc("@info", "An error occurred while loading the certifications: " "%1", - QString::fromLocal8Bit(result.error().asString())), + Formatting::errorAsString(result.error())), i18nc("@title", "Certifications Loading Failed")); } d->keyListJob = nullptr; } diff --git a/src/newcertificatewizard/keycreationpage.cpp b/src/newcertificatewizard/keycreationpage.cpp index 81fe38bb0..e39e5ec62 100644 --- a/src/newcertificatewizard/keycreationpage.cpp +++ b/src/newcertificatewizard/keycreationpage.cpp @@ -1,253 +1,253 @@ /* -*- mode: c++; c-basic-offset:4 -*- newcertificatewizard/keycreationpage.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016, 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 "keycreationpage_p.h" #include "keyalgo_p.h" #include "kleopatraapplication.h" #include "utils/keyparameters.h" #include "utils/keyusage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::NewCertificateUi; using namespace GpgME; struct KeyCreationPage::UI { UI(QWizardPage *parent) { parent->setTitle(i18nc("@title", "Creating Key Pair...")); auto mainLayout = new QVBoxLayout{parent}; auto label = new QLabel{i18n("The process of creating a key requires large amounts of random numbers. This may require several minutes..."), parent}; label->setWordWrap(true); mainLayout->addWidget(label); } }; KeyCreationPage::KeyCreationPage(QWidget *p) : WizardPage{p} , ui{new UI{this}} { setObjectName(QString::fromUtf8("Kleo__NewCertificateUi__KeyCreationPage")); } KeyCreationPage::~KeyCreationPage() = default; bool KeyCreationPage::isComplete() const { return !job; } void KeyCreationPage::initializePage() { startJob(); } void KeyCreationPage::startJob() { const auto proto = pgp() ? QGpgME::openpgp() : QGpgME::smime(); if (!proto) { return; } QGpgME::KeyGenerationJob *const j = proto->keyGenerationJob(); if (!j) { return; } if (!protectedKey() && pgp()) { auto ctx = QGpgME::Job::context(j); ctx->setPassphraseProvider(&mEmptyPassphraseProvider); ctx->setPinentryMode(Context::PinentryLoopback); } connect(j, &QGpgME::KeyGenerationJob::result, this, &KeyCreationPage::slotResult); if (const Error err = j->start(createGnupgKeyParms())) setField(QStringLiteral("error"), i18n("Could not start key pair creation: %1", - QString::fromLocal8Bit(err.asString()))); + Formatting::errorAsString(err))); else { job = j; } } KeyUsage KeyCreationPage::keyUsage() const { KeyUsage usage; if (signingAllowed()) { usage.setCanSign(true); } if (encryptionAllowed() && !is_ecdh(subkeyType()) && !is_dsa(keyType()) && !is_rsa(subkeyType())) { usage.setCanEncrypt(true); } if (authenticationAllowed()) { usage.setCanAuthenticate(true); } if (!usage.value() && certificationAllowed()) { /* Empty usages cause an error so we need to * add at least certify if nothing else is selected */ usage.setCanCertify(true); } return usage; } KeyUsage KeyCreationPage::subkeyUsage() const { KeyUsage usage; if (encryptionAllowed() && (is_dsa(keyType()) || is_rsa(subkeyType()) || is_ecdh(subkeyType()))) { Q_ASSERT(subkeyType()); usage.setCanEncrypt(true); } return usage; } QString KeyCreationPage::createGnupgKeyParms() const { KeyParameters keyParameters(pgp() ? KeyParameters::OpenPGP : KeyParameters::CMS); keyParameters.setKeyType(keyType()); if (is_ecdsa(keyType()) || is_eddsa(keyType())) { keyParameters.setKeyCurve(keyCurve()); } else if (const unsigned int strength = keyStrength()) { keyParameters.setKeyLength(strength); } keyParameters.setKeyUsage(keyUsage()); if (subkeyType()) { keyParameters.setSubkeyType(subkeyType()); if (is_ecdh(subkeyType())) { keyParameters.setSubkeyCurve(subkeyCurve()); } else if (const unsigned int strength = subkeyStrength()) { keyParameters.setSubkeyLength(strength); } keyParameters.setSubkeyUsage(subkeyUsage()); } if (pgp()) { if (expiryDate().isValid()) { keyParameters.setExpirationDate(expiryDate()); } if (!name().isEmpty()) { keyParameters.setName(name()); } if (!email().isEmpty()) { keyParameters.setEmail(email()); } } else { keyParameters.setDN(dn()); keyParameters.setEmail(email()); const auto addesses{additionalEMailAddresses()}; for (const QString &email : addesses) { keyParameters.addEmail(email); } const auto dnsN{dnsNames()}; for (const QString &dns : dnsN) { keyParameters.addDomainName(dns); } const auto urisList{uris()}; for (const QString &uri : urisList) { keyParameters.addURI(uri); } } const QString result = keyParameters.toString(); qCDebug(KLEOPATRA_LOG) << '\n' << result; return result; } void KeyCreationPage::slotResult(const GpgME::KeyGenerationResult &result, const QByteArray &request, const QString &auditLog) { Q_UNUSED(auditLog) if (result.error().code() || (pgp() && !result.fingerprint())) { setField(QStringLiteral("error"), result.error().isCanceled() ? i18n("Operation canceled.") : i18n("Could not create key pair: %1", - QString::fromLocal8Bit(result.error().asString()))); + Formatting::errorAsString(result.error()))); setField(QStringLiteral("url"), QString()); setField(QStringLiteral("result"), QString()); } else if (pgp()) { setField(QStringLiteral("error"), QString()); setField(QStringLiteral("url"), QString()); setField(QStringLiteral("result"), i18n("Key pair created successfully.\n" "Fingerprint: %1", Formatting::prettyID(result.fingerprint()))); } else { QFile file(tmpDir().absoluteFilePath(QStringLiteral("request.p10"))); if (!file.open(QIODevice::WriteOnly)) { setField(QStringLiteral("error"), i18n("Could not write output file %1: %2", file.fileName(), file.errorString())); setField(QStringLiteral("url"), QString()); setField(QStringLiteral("result"), QString()); } else { file.write(request); setField(QStringLiteral("error"), QString()); setField(QStringLiteral("url"), QUrl::fromLocalFile(file.fileName()).toString()); setField(QStringLiteral("result"), i18n("Key pair created successfully.")); } } // Ensure that we have the key in the keycache if (pgp() && !result.error().code() && result.fingerprint()) { auto ctx = Context::createForProtocol(OpenPGP); if (ctx) { // Check is pretty useless something very buggy in that case. Error e; const auto key = ctx->key(result.fingerprint(), e, true); if (!key.isNull()) { KeyCache::mutableInstance()->insert(key); } else { qCDebug(KLEOPATRA_LOG) << "Failed to find newly generated key."; } delete ctx; } } setField(QStringLiteral("fingerprint"), result.fingerprint() ? QString::fromLatin1(result.fingerprint()) : QString()); job = nullptr; Q_EMIT completeChanged(); const KConfigGroup config(KSharedConfig::openConfig(), "CertificateCreationWizard"); if (config.readEntry("SkipResultPage", false)) { if (result.fingerprint()) { KleopatraApplication::instance()->slotActivateRequested(QStringList() << QStringLiteral("kleopatra") << QStringLiteral("--query") << QLatin1String(result.fingerprint()), QString()); QMetaObject::invokeMethod(wizard(), "close", Qt::QueuedConnection); } else { QMetaObject::invokeMethod(wizard(), "next", Qt::QueuedConnection); } } else { QMetaObject::invokeMethod(wizard(), "next", Qt::QueuedConnection); } } diff --git a/src/selftest/gpgagentcheck.cpp b/src/selftest/gpgagentcheck.cpp index 7814af823..33e7b53c4 100644 --- a/src/selftest/gpgagentcheck.cpp +++ b/src/selftest/gpgagentcheck.cpp @@ -1,99 +1,101 @@ /* -*- mode: c++; c-basic-offset:4 -*- selftest/gpgagentcheck.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 "gpgagentcheck.h" #include "implementation_p.h" +#include + #include #include // for Qt::escape #include using namespace Kleo; using namespace Kleo::_detail; using namespace GpgME; namespace { class GpgAgentCheck : public SelfTestImplementation { public: explicit GpgAgentCheck() : SelfTestImplementation(i18nc("@title", "Gpg-Agent Connectivity")) { runTest(); } void runTest() { m_skipped = true; if (!hasFeature(AssuanEngineFeature, 0)) { m_error = i18n("GpgME library too old"); m_explanation = i18nc("@info", "Either the GpgME library itself is too old, " "or the GpgME++ library was compiled against " "an older GpgME that did not support connecting to gpg-agent."); m_proposedFix = xi18nc("@info", "Upgrade to gpgme 1.2.0 or higher, " "and ensure that gpgme++ was compiled against it."); } else if (ensureEngineVersion(GpgME::GpgConfEngine, 2, 1, 0)) { // 2.1 starts the agent on demand and requires it. So for 2.1.0 we can assume // autostart works and we don't need to care about the agent. m_skipped = false; m_passed = true; return; } else { Error error; const std::unique_ptr ctx = Context::createForEngine(AssuanEngine, &error); if (!ctx.get()) { m_error = i18n("GpgME does not support gpg-agent"); m_explanation = xi18nc("@info", "The GpgME library is new " "enough to support gpg-agent, " "but does not seem to do so in this installation." "The error returned was: %1.", - QString::fromLocal8Bit(error.asString()).toHtmlEscaped()); + Formatting::errorAsString(error).toHtmlEscaped()); // PENDING(marc) proposed fix? } else { m_skipped = false; const Error error = ctx->assuanTransact("GETINFO version"); if (error) { m_passed = false; m_error = i18n("unexpected error"); m_explanation = xi18nc("@info", "Unexpected error while asking gpg-agent " "for its version." "The error returned was: %1.", - QString::fromLocal8Bit(error.asString()).toHtmlEscaped()); + Formatting::errorAsString(error).toHtmlEscaped()); // PENDING(marc) proposed fix? } else { m_passed = true; } } } } }; } std::shared_ptr Kleo::makeGpgAgentConnectivitySelfTest() { return std::shared_ptr(new GpgAgentCheck); } diff --git a/src/smartcard/netkeycard.cpp b/src/smartcard/netkeycard.cpp index 822dc2760..bdda91873 100644 --- a/src/smartcard/netkeycard.cpp +++ b/src/smartcard/netkeycard.cpp @@ -1,123 +1,125 @@ /* 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 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:" << err.asString(); + 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); if (key.isNull()) { setCanLearnKeys(true); } 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; } diff --git a/src/smartcard/readerstatus.cpp b/src/smartcard/readerstatus.cpp index 602d02c3f..fdab3eeb6 100644 --- a/src/smartcard/readerstatus.cpp +++ b/src/smartcard/readerstatus.cpp @@ -1,1175 +1,1176 @@ /* -*- mode: c++; c-basic-offset:4 -*- smartcard/readerstatus.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "readerstatus.h" #include "deviceinfowatcher.h" #include +#include #include #include #include #include #include #include #include #include #include #include "openpgpcard.h" #include "netkeycard.h" #include "pivcard.h" #include "p15card.h" #include #include #include #include #include #include "utils/kdtoolsglobal.h" #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::SmartCard; using namespace GpgME; static ReaderStatus *self = nullptr; #define xtoi_1(p) (*(p) <= '9'? (*(p)- '0'): \ *(p) <= 'F'? (*(p)-'A'+10):(*(p)-'a'+10)) #define xtoi_2(p) ((xtoi_1(p) * 16) + xtoi_1((p)+1)) static const char *flags[] = { "NOCARD", "PRESENT", "ACTIVE", "USABLE", }; static_assert(sizeof flags / sizeof * flags == Card::_NumScdStates, ""); static const char *prettyFlags[] = { "NoCard", "CardPresent", "CardActive", "CardUsable", "CardError", }; static_assert(sizeof prettyFlags / sizeof * prettyFlags == Card::NumStates, ""); Q_DECLARE_METATYPE(GpgME::Error) #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) static QDebug operator<<(QDebug s, const std::string &string) { return s << QString::fromStdString(string); } #endif namespace { static bool gpgHasMultiCardMultiAppSupport() { return !(engineInfo(GpgME::GpgEngine).engineVersion() < "2.3.0"); } static QDebug operator<<(QDebug s, const std::vector< std::pair > &v) { using pair = std::pair; s << '('; for (const pair &p : v) { s << "status(" << QString::fromStdString(p.first) << ") =" << QString::fromStdString(p.second) << '\n'; } return s << ')'; } struct CardApp { std::string serialNumber; std::string appName; }; static void logUnexpectedStatusLine(const std::pair &line, const std::string &prefix = std::string(), const std::string &command = std::string()) { qCWarning(KLEOPATRA_LOG) << (!prefix.empty() ? QString::fromStdString(prefix + ": ") : QString()) << "Unexpected status line" << (!command.empty() ? QString::fromStdString(" on " + command + ":") : QLatin1String(":")) << QString::fromStdString(line.first) << QString::fromStdString(line.second); } static int parse_app_version(const std::string &s) { return std::atoi(s.c_str()); } static Card::PinState parse_pin_state(const QString &s) { bool ok; int i = s.toInt(&ok); if (!ok) { qCDebug(KLEOPATRA_LOG) << "Failed to parse pin state" << s; return Card::UnknownPinState; } switch (i) { case -4: return Card::NullPin; case -3: return Card::PinBlocked; case -2: return Card::NoPin; case -1: return Card::UnknownPinState; default: if (i < 0) { return Card::UnknownPinState; } else { return Card::PinOk; } } } static const std::string scd_getattr_status(std::shared_ptr &gpgAgent, const char *what, Error &err) { std::string cmd = "SCD GETATTR "; cmd += what; return Assuan::sendStatusCommand(gpgAgent, cmd.c_str(), err); } static const std::string getAttribute(std::shared_ptr &gpgAgent, const char *attribute, const char *versionHint) { Error err; const auto result = scd_getattr_status(gpgAgent, attribute, err); if (err) { if (err.code() == GPG_ERR_INV_NAME) { qCDebug(KLEOPATRA_LOG) << "Querying for attribute" << attribute << "not yet supported; needs GnuPG" << versionHint; } else { qCWarning(KLEOPATRA_LOG) << "Running SCD GETATTR " << attribute << " failed:" << err; } return std::string(); } return result; } static std::vector getCardsAndApps(std::shared_ptr &gpgAgent, Error &err) { std::vector result; if (gpgHasMultiCardMultiAppSupport()) { const std::string command = "SCD GETINFO all_active_apps"; const auto statusLines = Assuan::sendStatusLinesCommand(gpgAgent, command.c_str(), err); if (err) { return result; } for (const auto &statusLine: statusLines) { if (statusLine.first == "SERIALNO") { const auto serialNumberAndApps = QByteArray::fromStdString(statusLine.second).split(' '); if (serialNumberAndApps.size() >= 2) { const auto serialNumber = serialNumberAndApps[0]; auto apps = serialNumberAndApps.mid(1); // sort the apps to get a stable order independently of the currently selected application std::sort(apps.begin(), apps.end()); for (const auto &app: apps) { qCDebug(KLEOPATRA_LOG) << "getCardsAndApps(): Found card" << serialNumber << "with app" << app; result.push_back({ serialNumber.toStdString(), app.toStdString() }); } } else { logUnexpectedStatusLine(statusLine, "getCardsAndApps()", command); } } else { logUnexpectedStatusLine(statusLine, "getCardsAndApps()", command); } } } else { // use SCD SERIALNO to get the currently active card const auto serialNumber = Assuan::sendStatusCommand(gpgAgent, "SCD SERIALNO", err); if (err) { return result; } // use SCD GETATTR APPTYPE to find out which app is active auto appName = scd_getattr_status(gpgAgent, "APPTYPE", err); std::transform(appName.begin(), appName.end(), appName.begin(), [](unsigned char c){ return std::tolower(c); }); if (err) { return result; } result.push_back({ serialNumber, appName }); } return result; } static std::string switchCard(std::shared_ptr &gpgAgent, const std::string &serialNumber, Error &err) { const std::string command = "SCD SWITCHCARD " + serialNumber; const auto statusLines = Assuan::sendStatusLinesCommand(gpgAgent, command.c_str(), err); if (err) { return std::string(); } if (statusLines.size() == 1 && statusLines[0].first == "SERIALNO" && statusLines[0].second == serialNumber) { return serialNumber; } qCWarning(KLEOPATRA_LOG) << "switchCard():" << command << "returned" << statusLines << "(expected:" << "SERIALNO " + serialNumber << ")"; return std::string(); } static std::string switchApp(std::shared_ptr &gpgAgent, const std::string &serialNumber, const std::string &appName, Error &err) { const std::string command = "SCD SWITCHAPP " + appName; const auto statusLines = Assuan::sendStatusLinesCommand(gpgAgent, command.c_str(), err); if (err) { return std::string(); } if (statusLines.size() == 1 && statusLines[0].first == "SERIALNO" && statusLines[0].second.find(serialNumber + ' ' + appName) == 0) { return appName; } qCWarning(KLEOPATRA_LOG) << "switchApp():" << command << "returned" << statusLines << "(expected:" << "SERIALNO " + serialNumber + ' ' + appName + "..." << ")"; return std::string(); } static const char * get_openpgp_card_manufacturer_from_serial_number(const std::string &serialno) { qCDebug(KLEOPATRA_LOG) << "get_openpgp_card_manufacturer_from_serial_number(" << serialno.c_str() << ")"; const bool isProperOpenPGPCardSerialNumber = serialno.size() == 32 && serialno.substr(0, 12) == "D27600012401"; if (isProperOpenPGPCardSerialNumber) { const char *sn = serialno.c_str(); const int manufacturerId = xtoi_2(sn + 16)*256 + xtoi_2(sn + 18); switch (manufacturerId) { case 0x0001: return "PPC Card Systems"; case 0x0002: return "Prism"; case 0x0003: return "OpenFortress"; case 0x0004: return "Wewid"; case 0x0005: return "ZeitControl"; case 0x0006: return "Yubico"; case 0x0007: return "OpenKMS"; case 0x0008: return "LogoEmail"; case 0x002A: return "Magrathea"; case 0x1337: return "Warsaw Hackerspace"; case 0xF517: return "FSIJ"; /* 0x0000 and 0xFFFF are defined as test cards per spec, 0xFF00 to 0xFFFE are assigned for use with randomly created serial numbers. */ case 0x0000: case 0xffff: return "test card"; default: return (manufacturerId & 0xff00) == 0xff00 ? "unmanaged S/N range" : "unknown"; } } else { return "unknown"; } } static std::vector get_openpgp_card_supported_algorithms_announced_by_card(std::shared_ptr &gpgAgent) { static constexpr std::string_view cardSlotPrefix = "OPENPGP.1 "; static const std::map algoMapping = { {"cv25519", "curve25519"}, {"cv448", "curve448"}, {"ed25519", "curve25519"}, {"ed448", "curve448"}, {"x448", "curve448"}, }; Error err; const auto lines = Assuan::sendStatusLinesCommand(gpgAgent, "SCD GETATTR KEY-ATTR-INFO", err); if (err) { return {}; } std::vector algos; kdtools::transform_if(lines.cbegin(), lines.cend(), std::back_inserter(algos), [](const auto &line) { auto algo = line.second.substr(cardSlotPrefix.size()); // map a few algorithms to the standard names used by us const auto mapping = algoMapping.find(algo); if (mapping != algoMapping.end()) { algo = mapping->second; } return algo; }, [](const auto &line) { // only consider KEY-ATTR-INFO status lines for the first card slot; // for now, we assume that all card slots support the same algorithms return line.first == "KEY-ATTR-INFO" && line.second.starts_with(cardSlotPrefix); }); // remove duplicate algorithms std::sort(algos.begin(), algos.end()); algos.erase(std::unique(algos.begin(), algos.end()), algos.end()); qCDebug(KLEOPATRA_LOG) << __func__ << "returns" << algos; return algos; } static std::vector get_openpgp_card_supported_algorithms(Card *card, std::shared_ptr &gpgAgent) { // first ask the smart card for the supported algorithms const std::vector announcedAlgos = get_openpgp_card_supported_algorithms_announced_by_card(gpgAgent); if (!announcedAlgos.empty()) { return announcedAlgos; } // otherwise, fall back to hard-coded lists if ((card->cardType() == "yubikey") && (card->cardVersion() >= 0x050203)) { return { "rsa2048", "rsa3072", "rsa4096", "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", "curve25519", }; } else if ((card->cardType() == "zeitcontrol") && (card->appVersion() >= 0x0304)) { return { "rsa2048", "rsa3072", "rsa4096", "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", }; } return { "rsa2048", "rsa3072", "rsa4096" }; } static bool isOpenPGPCardSerialNumber(const std::string &serialNumber) { return serialNumber.size() == 32 && serialNumber.substr(0, 12) == "D27600012401"; } static const std::string getDisplaySerialNumber(std::shared_ptr &gpgAgent, Error &err) { const auto displaySerialNumber = scd_getattr_status(gpgAgent, "$DISPSERIALNO", err); if (err && err.code() != GPG_ERR_INV_NAME) { qCWarning(KLEOPATRA_LOG) << "Running SCD GETATTR $DISPSERIALNO failed:" << err; } return displaySerialNumber; } static void setDisplaySerialNumber(Card *card, std::shared_ptr &gpgAgent) { static const QRegularExpression leadingZeros(QStringLiteral("^0*")); Error err; const QString displaySerialNumber = QString::fromStdString(getDisplaySerialNumber(gpgAgent, err)); if (err) { card->setDisplaySerialNumber(QString::fromStdString(card->serialNumber())); return; } if (isOpenPGPCardSerialNumber(card->serialNumber()) && displaySerialNumber.size() == 12) { // add a space between manufacturer id and card id for OpenPGP cards card->setDisplaySerialNumber(displaySerialNumber.left(4) + QLatin1Char(' ') + displaySerialNumber.right(8)); } else { card->setDisplaySerialNumber(displaySerialNumber); } return; } static void learnCardKeyStubs(const Card *card, std::shared_ptr &gpg_agent) { for (const KeyPairInfo &keyInfo : card->keyInfos()) { if (!keyInfo.grip.empty()) { Error err; const auto command = std::string("READKEY --card --no-data -- ") + keyInfo.keyRef; (void)Assuan::sendStatusLinesCommand(gpg_agent, command.c_str(), err); if (err) { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; } } } } static void handle_openpgp_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto pgpCard = new OpenPGPCard(*ci); const auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err.code()) { ci->setStatus(Card::CardError); return; } pgpCard->setCardInfo(info); if (pgpCard->manufacturer().empty()) { // fallback in case MANUFACTURER is not yet included in the card info pgpCard->setManufacturer(get_openpgp_card_manufacturer_from_serial_number(ci->serialNumber())); } setDisplaySerialNumber(pgpCard, gpg_agent); learnCardKeyStubs(pgpCard, gpg_agent); pgpCard->setSupportedAlgorithms(get_openpgp_card_supported_algorithms(pgpCard, gpg_agent)); ci.reset(pgpCard); } static void readKeyPairInfoFromPIVCard(const std::string &keyRef, PIVCard *pivCard, const std::shared_ptr &gpg_agent) { Error err; const std::string command = std::string("SCD READKEY --info-only -- ") + keyRef; const auto keyPairInfoLines = Assuan::sendStatusLinesCommand(gpg_agent, command.c_str(), err); if (err) { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; return; } // this adds the key algorithm (and the key creation date, but that seems to be unset for PIV) to the existing key pair information pivCard->setCardInfo(keyPairInfoLines); } static void readCertificateFromPIVCard(const std::string &keyRef, PIVCard *pivCard, const std::shared_ptr &gpg_agent) { Error err; const std::string command = std::string("SCD READCERT ") + keyRef; const std::string certificateData = Assuan::sendDataCommand(gpg_agent, command.c_str(), err); if (err && err.code() != GPG_ERR_NOT_FOUND) { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; return; } if (certificateData.empty()) { qCDebug(KLEOPATRA_LOG) << "readCertificateFromPIVCard(" << QString::fromStdString(keyRef) << "): No certificate stored on card"; return; } qCDebug(KLEOPATRA_LOG) << "readCertificateFromPIVCard(" << QString::fromStdString(keyRef) << "): Found certificate stored on card"; pivCard->setCertificateData(keyRef, certificateData); } static void handle_piv_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto pivCard = new PIVCard(*ci); const auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err) { ci->setStatus(Card::CardError); return; } pivCard->setCardInfo(info); setDisplaySerialNumber(pivCard, gpg_agent); for (const KeyPairInfo &keyInfo : pivCard->keyInfos()) { if (!keyInfo.grip.empty()) { readKeyPairInfoFromPIVCard(keyInfo.keyRef, pivCard, gpg_agent); readCertificateFromPIVCard(keyInfo.keyRef, pivCard, gpg_agent); } } learnCardKeyStubs(pivCard, gpg_agent); ci.reset(pivCard); } static void handle_p15_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto p15Card = new P15Card(*ci); auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err) { ci->setStatus(Card::CardError); return; } const auto fprs = Assuan::sendStatusLinesCommand(gpg_agent, "SCD GETATTR KEY-FPR", err); if (!err) { info.insert(info.end(), fprs.begin(), fprs.end()); } p15Card->setCardInfo(info); learnCardKeyStubs(p15Card, gpg_agent); setDisplaySerialNumber(p15Card, gpg_agent); ci.reset(p15Card); } static void handle_netkey_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto nkCard = new NetKeyCard(*ci); ci.reset(nkCard); ci->setAppVersion(parse_app_version(scd_getattr_status(gpg_agent, "NKS-VERSION", err))); if (err.code()) { qCWarning(KLEOPATRA_LOG) << "Running SCD GETATTR NKS-VERSION failed:" << err; - ci->setErrorMsg(QStringLiteral ("NKS-VERSION failed: ") + QString::fromUtf8(err.asString())); + ci->setErrorMsg(QStringLiteral ("NKS-VERSION failed: ") + Formatting::errorAsString(err)); return; } if (ci->appVersion() < 3) { qCDebug(KLEOPATRA_LOG) << "not a NetKey v3 (or later) card, giving up. Version:" << ci->appVersion(); ci->setErrorMsg(QStringLiteral("NetKey v%1 cards are not supported.").arg(ci->appVersion())); return; } setDisplaySerialNumber(nkCard, gpg_agent); // the following only works for NKS v3... const auto chvStatus = QString::fromStdString( scd_getattr_status(gpg_agent, "CHV-STATUS", err)).split(QLatin1Char(' ')); if (err.code()) { qCDebug(KLEOPATRA_LOG) << "Running SCD GETATTR CHV-STATUS failed:" << err; - ci->setErrorMsg(QStringLiteral ("CHV-Status failed: ") + QString::fromUtf8(err.asString())); + ci->setErrorMsg(QStringLiteral ("CHV-Status failed: ") + Formatting::errorAsString(err)); return; } std::vector states; states.reserve(chvStatus.count()); // CHV Status for NKS v3 is // Pin1 (Normal pin) Pin2 (Normal PUK) // SigG1 SigG PUK. int num = 0; for (const auto &state: chvStatus) { const auto parsed = parse_pin_state (state); states.push_back(parsed); if (parsed == Card::NullPin) { if (num == 0) { ci->setHasNullPin(true); } } ++num; } nkCard->setPinStates(states); const auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err) { ci->setStatus(Card::CardError); return; } nkCard->setCardInfo(info); learnCardKeyStubs(nkCard, gpg_agent); } static std::shared_ptr get_card_status(const std::string &serialNumber, const std::string &appName, std::shared_ptr &gpg_agent) { qCDebug(KLEOPATRA_LOG) << "get_card_status(" << serialNumber << ',' << appName << ',' << gpg_agent.get() << ')'; auto ci = std::shared_ptr(new Card()); if (gpgHasMultiCardMultiAppSupport()) { // select card Error err; const auto result = switchCard(gpg_agent, serialNumber, err); if (err) { if (err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED) { ci->setStatus(Card::NoCard); } else { ci->setStatus(Card::CardError); } return ci; } if (result.empty()) { qCWarning(KLEOPATRA_LOG) << "get_card_status: switching card failed"; ci->setStatus(Card::CardError); return ci; } ci->setStatus(Card::CardPresent); } else { ci->setStatus(Card::CardPresent); } if (gpgHasMultiCardMultiAppSupport()) { // select app Error err; const auto result = switchApp(gpg_agent, serialNumber, appName, err); if (err) { if (err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED) { ci->setStatus(Card::NoCard); } else { ci->setStatus(Card::CardError); } return ci; } if (result.empty()) { qCWarning(KLEOPATRA_LOG) << "get_card_status: switching app failed"; ci->setStatus(Card::CardError); return ci; } } ci->setSerialNumber(serialNumber); ci->setSigningKeyRef(getAttribute(gpg_agent, "$SIGNKEYID", "2.2.18")); ci->setEncryptionKeyRef(getAttribute(gpg_agent, "$ENCRKEYID", "2.2.18")); // Handle different card types if (appName == NetKeyCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found Netkey card" << ci->serialNumber().c_str() << "end"; handle_netkey_card(ci, gpg_agent); return ci; } else if (appName == OpenPGPCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found OpenPGP card" << ci->serialNumber().c_str() << "end"; ci->setAuthenticationKeyRef(OpenPGPCard::pgpAuthKeyRef()); handle_openpgp_card(ci, gpg_agent); return ci; } else if (appName == PIVCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found PIV card" << ci->serialNumber().c_str() << "end"; handle_piv_card(ci, gpg_agent); return ci; } else if (appName == P15Card::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found P15 card" << ci->serialNumber().c_str() << "end"; handle_p15_card(ci, gpg_agent); return ci; } else { qCDebug(KLEOPATRA_LOG) << "get_card_status: unhandled application:" << appName; return ci; } return ci; } static bool isCardNotPresentError(const GpgME::Error &err) { // see fixup_scd_errors() in gpg-card.c return err && ((err.code() == GPG_ERR_CARD_NOT_PRESENT) || ((err.code() == GPG_ERR_ENODEV || err.code() == GPG_ERR_CARD_REMOVED) && (err.sourceID() == GPG_ERR_SOURCE_SCD))); } static std::vector > update_cardinfo(std::shared_ptr &gpgAgent) { qCDebug(KLEOPATRA_LOG) << "update_cardinfo()"; // ensure that a card is present and that all cards are properly set up { Error err; const char *command = (gpgHasMultiCardMultiAppSupport()) ? "SCD SERIALNO --all" : "SCD SERIALNO"; const std::string serialno = Assuan::sendStatusCommand(gpgAgent, command, err); if (err) { if (isCardNotPresentError(err)) { qCDebug(KLEOPATRA_LOG) << "update_cardinfo: No card present"; return std::vector >(); } else { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; auto ci = std::shared_ptr(new Card()); ci->setStatus(Card::CardError); return std::vector >(1, ci); } } } Error err; const std::vector cardApps = getCardsAndApps(gpgAgent, err); if (err) { if (isCardNotPresentError(err)) { qCDebug(KLEOPATRA_LOG) << "update_cardinfo: No card present"; return std::vector >(); } else { qCWarning(KLEOPATRA_LOG) << "Getting active apps on all inserted cards failed:" << err; auto ci = std::shared_ptr(new Card()); ci->setStatus(Card::CardError); return std::vector >(1, ci); } } std::vector > cards; for (const auto &cardApp: cardApps) { const auto card = get_card_status(cardApp.serialNumber, cardApp.appName, gpgAgent); cards.push_back(card); } return cards; } } // namespace struct Transaction { CardApp cardApp; QByteArray command; QPointer receiver; ReaderStatus::TransactionFunc slot; AssuanTransaction* assuanTransaction; }; static const Transaction updateTransaction = { { "__all__", "__all__" }, "__update__", nullptr, nullptr, nullptr }; static const Transaction quitTransaction = { { "__all__", "__all__" }, "__quit__", nullptr, nullptr, nullptr }; namespace { class ReaderStatusThread : public QThread { Q_OBJECT public: explicit ReaderStatusThread(QObject *parent = nullptr) : QThread(parent), m_gnupgHomePath(Kleo::gnupgHomeDirectory()), m_transactions(1, updateTransaction) // force initial scan { connect(this, &ReaderStatusThread::oneTransactionFinished, this, &ReaderStatusThread::slotOneTransactionFinished); } std::vector > cardInfos() const { const QMutexLocker locker(&m_mutex); return m_cardInfos; } Card::Status cardStatus(unsigned int slot) const { const QMutexLocker locker(&m_mutex); if (slot < m_cardInfos.size()) { return m_cardInfos[slot]->status(); } else { return Card::NoCard; } } void addTransaction(const Transaction &t) { const QMutexLocker locker(&m_mutex); m_transactions.push_back(t); m_waitForTransactions.wakeOne(); } Q_SIGNALS: void firstCardWithNullPinChanged(const std::string &serialNumber); void anyCardCanLearnKeysChanged(bool); void cardAdded(const std::string &serialNumber, const std::string &appName); void cardChanged(const std::string &serialNumber, const std::string &appName); void cardRemoved(const std::string &serialNumber, const std::string &appName); void updateFinished(); void oneTransactionFinished(const GpgME::Error &err); public Q_SLOTS: void deviceStatusChanged(const QByteArray &details) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[GUI]::deviceStatusChanged(" << details << ")"; addTransaction(updateTransaction); } void ping() { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[GUI]::ping()"; addTransaction(updateTransaction); } void stop() { const QMutexLocker locker(&m_mutex); m_transactions.push_front(quitTransaction); m_waitForTransactions.wakeOne(); } private Q_SLOTS: void slotOneTransactionFinished(const GpgME::Error &err) { std::list ft; KDAB_SYNCHRONIZED(m_mutex) ft.splice(ft.begin(), m_finishedTransactions); for (const Transaction &t : std::as_const(ft)) if (t.receiver && t.slot) { QMetaObject::invokeMethod(t.receiver, [&t, &err]() { t.slot(err); }, Qt::DirectConnection); } } private: void run() override { while (true) { std::shared_ptr gpgAgent; CardApp cardApp; QByteArray command; bool nullSlot = false; AssuanTransaction* assuanTransaction = nullptr; std::list item; std::vector > oldCards; while (!KeyCache::instance()->initialized()) { qCDebug(KLEOPATRA_LOG) << "Waiting for Keycache to be initialized."; sleep(1); } Error err; std::unique_ptr c = Context::createForEngine(AssuanEngine, &err); if (err.code() == GPG_ERR_NOT_SUPPORTED) { return; } gpgAgent = std::shared_ptr(c.release()); KDAB_SYNCHRONIZED(m_mutex) { while (m_transactions.empty()) { // go to sleep waiting for more work: qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[2nd]: waiting for commands"; m_waitForTransactions.wait(&m_mutex); } // splice off the first transaction without // copying, so we own it without really importing // it into this thread (the QPointer isn't // thread-safe): item.splice(item.end(), m_transactions, m_transactions.begin()); // make local copies of the interesting stuff so // we can release the mutex again: cardApp = item.front().cardApp; command = item.front().command; nullSlot = !item.front().slot; // we take ownership of the assuan transaction std::swap(assuanTransaction, item.front().assuanTransaction); oldCards = m_cardInfos; } qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[2nd]: new iteration command=" << command << " ; nullSlot=" << nullSlot; // now, let's see what we got: if (nullSlot && command == quitTransaction.command) { return; // quit } if ((nullSlot && command == updateTransaction.command)) { bool anyError = false; if (cardApp.serialNumber == "__all__" || cardApp.appName == "__all__") { std::vector > newCards = update_cardinfo(gpgAgent); KDAB_SYNCHRONIZED(m_mutex) { m_cardInfos = newCards; } bool anyLC = false; std::string firstCardWithNullPin; for (const auto &newCard: newCards) { const auto serialNumber = newCard->serialNumber(); const auto appName = newCard->appName(); const auto matchingOldCard = std::find_if(oldCards.cbegin(), oldCards.cend(), [serialNumber, appName] (const std::shared_ptr &card) { return card->serialNumber() == serialNumber && card->appName() == appName; }); if (matchingOldCard == oldCards.cend()) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "was added"; Q_EMIT cardAdded(serialNumber, appName); } else { if (*newCard != **matchingOldCard) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "changed"; Q_EMIT cardChanged(serialNumber, appName); } oldCards.erase(matchingOldCard); } if (newCard->canLearnKeys()) { anyLC = true; } if (newCard->hasNullPin() && firstCardWithNullPin.empty()) { firstCardWithNullPin = newCard->serialNumber(); } if (newCard->status() == Card::CardError) { anyError = true; } } for (const auto &oldCard: oldCards) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << oldCard->serialNumber() << "with app" << oldCard->appName() << "was removed"; Q_EMIT cardRemoved(oldCard->serialNumber(), oldCard->appName()); } Q_EMIT firstCardWithNullPinChanged(firstCardWithNullPin); Q_EMIT anyCardCanLearnKeysChanged(anyLC); } else { auto updatedCard = get_card_status(cardApp.serialNumber, cardApp.appName, gpgAgent); const auto serialNumber = updatedCard->serialNumber(); const auto appName = updatedCard->appName(); bool cardWasAdded = false; bool cardWasChanged = false; KDAB_SYNCHRONIZED(m_mutex) { const auto matchingCard = std::find_if(m_cardInfos.begin(), m_cardInfos.end(), [serialNumber, appName](const auto &card) { return card->serialNumber() == serialNumber && card->appName() == appName; }); if (matchingCard == m_cardInfos.end()) { m_cardInfos.push_back(updatedCard); cardWasAdded = true; } else { cardWasChanged = (*updatedCard != **matchingCard); m_cardInfos[std::distance(m_cardInfos.begin(), matchingCard)] = updatedCard; } if (updatedCard->status() == Card::CardError) { anyError = true; } } if (cardWasAdded) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "was added"; Q_EMIT cardAdded(serialNumber, appName); } else if (cardWasChanged) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "changed"; Q_EMIT cardChanged(serialNumber, appName); } } if (anyError) { gpgAgent.reset(); } Q_EMIT updateFinished(); } else { GpgME::Error err; if (gpgHasMultiCardMultiAppSupport()) { switchCard(gpgAgent, cardApp.serialNumber, err); if (!err) { switchApp(gpgAgent, cardApp.serialNumber, cardApp.appName, err); } } if (!err) { if (assuanTransaction) { (void)Assuan::sendCommand(gpgAgent, command.constData(), std::unique_ptr(assuanTransaction), err); } else { (void)Assuan::sendCommand(gpgAgent, command.constData(), err); } } KDAB_SYNCHRONIZED(m_mutex) // splice 'item' into m_finishedTransactions: m_finishedTransactions.splice(m_finishedTransactions.end(), item); Q_EMIT oneTransactionFinished(err); } } } private: mutable QMutex m_mutex; QWaitCondition m_waitForTransactions; const QString m_gnupgHomePath; // protected by m_mutex: std::vector > m_cardInfos; std::list m_transactions, m_finishedTransactions; }; } class ReaderStatus::Private : ReaderStatusThread { friend class Kleo::SmartCard::ReaderStatus; ReaderStatus *const q; public: explicit Private(ReaderStatus *qq) : ReaderStatusThread(qq), q(qq), watcher() { KDAB_SET_OBJECT_NAME(watcher); qRegisterMetaType("Kleo::SmartCard::Card::Status"); qRegisterMetaType("GpgME::Error"); connect(this, &::ReaderStatusThread::cardAdded, q, &ReaderStatus::cardAdded); connect(this, &::ReaderStatusThread::cardChanged, q, &ReaderStatus::cardChanged); connect(this, &::ReaderStatusThread::cardRemoved, q, &ReaderStatus::cardRemoved); connect(this, &::ReaderStatusThread::updateFinished, q, &ReaderStatus::updateFinished); connect(this, &::ReaderStatusThread::firstCardWithNullPinChanged, q, &ReaderStatus::firstCardWithNullPinChanged); connect(this, &::ReaderStatusThread::anyCardCanLearnKeysChanged, q, &ReaderStatus::anyCardCanLearnKeysChanged); if (DeviceInfoWatcher::isSupported()) { qCDebug(KLEOPATRA_LOG) << "ReaderStatus::Private: Using new DeviceInfoWatcher"; connect(&devInfoWatcher, &DeviceInfoWatcher::statusChanged, this, &::ReaderStatusThread::deviceStatusChanged); } else { qCDebug(KLEOPATRA_LOG) << "ReaderStatus::Private: Using deprecated FileSystemWatcher"; watcher.whitelistFiles(QStringList(QStringLiteral("reader_*.status"))); watcher.addPath(Kleo::gnupgHomeDirectory()); watcher.setDelay(100); connect(&watcher, &FileSystemWatcher::triggered, this, &::ReaderStatusThread::ping); } } ~Private() override { stop(); if (!wait(100)) { terminate(); wait(); } } private: std::string firstCardWithNullPinImpl() const { const auto cis = cardInfos(); const auto firstWithNullPin = std::find_if(cis.cbegin(), cis.cend(), [](const std::shared_ptr &ci) { return ci->hasNullPin(); }); return firstWithNullPin != cis.cend() ? (*firstWithNullPin)->serialNumber() : std::string(); } bool anyCardCanLearnKeysImpl() const { const auto cis = cardInfos(); return std::any_of(cis.cbegin(), cis.cend(), [](const std::shared_ptr &ci) { return ci->canLearnKeys(); }); } private: FileSystemWatcher watcher; DeviceInfoWatcher devInfoWatcher; }; ReaderStatus::ReaderStatus(QObject *parent) : QObject(parent), d(new Private(this)) { self = this; qRegisterMetaType("std::string"); } ReaderStatus::~ReaderStatus() { self = nullptr; } // slot void ReaderStatus::startMonitoring() { d->start(); if (DeviceInfoWatcher::isSupported()) { connect(&d->devInfoWatcher, &DeviceInfoWatcher::startOfGpgAgentRequested, this, &ReaderStatus::startOfGpgAgentRequested); d->devInfoWatcher.start(); } } // static ReaderStatus *ReaderStatus::mutableInstance() { return self; } // static const ReaderStatus *ReaderStatus::instance() { return self; } Card::Status ReaderStatus::cardStatus(unsigned int slot) const { return d->cardStatus(slot); } std::string ReaderStatus::firstCardWithNullPin() const { return d->firstCardWithNullPinImpl(); } bool ReaderStatus::anyCardCanLearnKeys() const { return d->anyCardCanLearnKeysImpl(); } void ReaderStatus::startSimpleTransaction(const std::shared_ptr &card, const QByteArray &command, QObject *receiver, const TransactionFunc &slot) { const CardApp cardApp = { card->serialNumber(), card->appName() }; const Transaction t = { cardApp, command, receiver, slot, nullptr }; d->addTransaction(t); } void ReaderStatus::startTransaction(const std::shared_ptr &card, const QByteArray &command, QObject *receiver, const TransactionFunc &slot, std::unique_ptr transaction) { const CardApp cardApp = { card->serialNumber(), card->appName() }; const Transaction t = { cardApp, command, receiver, slot, transaction.release() }; d->addTransaction(t); } void ReaderStatus::updateStatus() { d->ping(); } void ReaderStatus::updateCard(const std::string &serialNumber, const std::string &appName) { const CardApp cardApp = { serialNumber, appName }; const Transaction t = { cardApp, updateTransaction.command, nullptr, nullptr, nullptr }; d->addTransaction(t); } std::vector > ReaderStatus::getCards() const { return d->cardInfos(); } std::shared_ptr ReaderStatus::getCard(const std::string &serialNumber, const std::string &appName) const { for (const auto &card: d->cardInfos()) { if (card->serialNumber() == serialNumber && card->appName() == appName) { qCDebug(KLEOPATRA_LOG) << "ReaderStatus::getCard() - Found card with serial number" << serialNumber << "and app" << appName; return card; } } qCWarning(KLEOPATRA_LOG) << "ReaderStatus::getCard() - Did not find card with serial number" << serialNumber << "and app" << appName; return std::shared_ptr(); } // static std::string ReaderStatus::switchCard(std::shared_ptr& ctx, const std::string& serialNumber, Error& err) { return ::switchCard(ctx, serialNumber, err); } // static std::string ReaderStatus::switchApp(std::shared_ptr& ctx, const std::string& serialNumber, const std::string& appName, Error& err) { return ::switchApp(ctx, serialNumber, appName, err); } // static Error ReaderStatus::switchCardAndApp(const std::string &serialNumber, const std::string &appName) { Error err; if (!(engineInfo(GpgEngine).engineVersion() < "2.3.0")) { std::unique_ptr c = Context::createForEngine(AssuanEngine, &err); if (err.code() == GPG_ERR_NOT_SUPPORTED) { return err; } auto assuanContext = std::shared_ptr(c.release()); const auto resultSerialNumber = switchCard(assuanContext, serialNumber, err); if (err || resultSerialNumber != serialNumber) { qCWarning(KLEOPATRA_LOG) << "Switching to card" << QString::fromStdString(serialNumber) << "failed"; if (!err) { err = Error::fromCode(GPG_ERR_UNEXPECTED); } return err; } const auto resultAppName = switchApp(assuanContext, serialNumber, appName, err); if (err || resultAppName != appName) { qCWarning(KLEOPATRA_LOG) << "Switching card to" << QString::fromStdString(appName) << "app failed"; if (!err) { err = Error::fromCode(GPG_ERR_UNEXPECTED); } return err; } } return err; } #include "readerstatus.moc" diff --git a/src/uiserver/assuanserverconnection.cpp b/src/uiserver/assuanserverconnection.cpp index 7c61d0dde..ced038311 100644 --- a/src/uiserver/assuanserverconnection.cpp +++ b/src/uiserver/assuanserverconnection.cpp @@ -1,1503 +1,1504 @@ /* -*- mode: c++; c-basic-offset:4 -*- uiserver/assuanserverconnection.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 */ #ifndef QT_NO_CAST_TO_ASCII # define QT_NO_CAST_TO_ASCII #endif #ifndef QT_NO_CAST_FROM_ASCII # define QT_NO_CAST_FROM_ASCII #endif #include #include #include "assuanserverconnection.h" #include "assuancommand.h" #include "sessiondata.h" #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 #include #include #include #include #ifdef __GLIBCXX__ # include // for is_sorted #endif #ifdef Q_OS_WIN # include # include #else # include # include #endif using namespace Kleo; static const unsigned int INIT_SOCKET_FLAGS = 3; // says info assuan... //static int(*USE_DEFAULT_HANDLER)(assuan_context_t,char*) = 0; static const int FOR_READING = 0; static const unsigned int MAX_ACTIVE_FDS = 32; static void my_assuan_release(assuan_context_t ctx) { if (ctx) { assuan_release(ctx); } } // std::shared_ptr for assuan_context_t w/ deleter enforced to assuan_deinit_server: using AssuanContextBase = std::shared_ptr::type>; struct AssuanContext : AssuanContextBase { AssuanContext() : AssuanContextBase() {} explicit AssuanContext(assuan_context_t ctx) : AssuanContextBase(ctx, &my_assuan_release) {} void reset(assuan_context_t ctx = nullptr) { AssuanContextBase::reset(ctx, &my_assuan_release); } }; static inline gpg_error_t assuan_process_done_msg(assuan_context_t ctx, gpg_error_t err, const char *err_msg) { return assuan_process_done(ctx, assuan_set_error(ctx, err, err_msg)); } static inline gpg_error_t assuan_process_done_msg(assuan_context_t ctx, gpg_error_t err, const std::string &err_msg) { return assuan_process_done_msg(ctx, err, err_msg.c_str()); } static inline gpg_error_t assuan_process_done_msg(assuan_context_t ctx, gpg_error_t err, const QString &err_msg) { return assuan_process_done_msg(ctx, err, err_msg.toUtf8().constData()); } static std::map upcase_option(const char *option, std::map options) { std::string value; bool value_found = false; auto it = options.begin(); while (it != options.end()) if (qstricmp(it->first.c_str(), option) == 0) { value = it->second; options.erase(it++); value_found = true; } else { ++it; } if (value_found) { options[option] = value; } return options; } static std::map parse_commandline(const char *line) { std::map result; if (line) { const char *begin = line; const char *lastEQ = nullptr; while (*line) { if (*line == ' ' || *line == '\t') { if (begin != line) { if (begin[0] == '-' && begin[1] == '-') { begin += 2; // skip initial "--" } if (lastEQ && lastEQ > begin) { result[ std::string(begin, lastEQ - begin) ] = hexdecode(std::string(lastEQ + 1, line - (lastEQ + 1))); } else { result[ std::string(begin, line - begin) ] = std::string(); } } begin = line + 1; } else if (*line == '=') { if (line == begin) throw Exception(gpg_error(GPG_ERR_ASS_SYNTAX), i18n("No option name given")); else { lastEQ = line; } } ++line; } if (begin != line) { if (begin[0] == '-' && begin[1] == '-') { begin += 2; // skip initial "--" } if (lastEQ && lastEQ > begin) { result[ std::string(begin, lastEQ - begin) ] = hexdecode(std::string(lastEQ + 1, line - (lastEQ + 1))); } else { result[ begin ] = std::string(); } } } return result; } static WId wid_from_string(const QString &winIdStr, bool *ok = nullptr) { return static_cast(winIdStr.toULongLong(ok, 16)); } static void apply_window_id(QWidget *widget, const QString &winIdStr) { if (!widget || winIdStr.isEmpty()) { return; } bool ok = false; const WId wid = wid_from_string(winIdStr, &ok); if (!ok) { qCDebug(KLEOPATRA_LOG) << "window-id value" << wid << "doesn't look like a number"; return; } if (QWidget *pw = QWidget::find(wid)) { widget->setParent(pw, widget->windowFlags()); } else { widget->setAttribute(Qt::WA_NativeWindow, true); KWindowSystem::setMainWindow(widget->windowHandle(), wid); } } // // // AssuanServerConnection: // // class AssuanServerConnection::Private : public QObject { Q_OBJECT friend class ::Kleo::AssuanServerConnection; friend class ::Kleo::AssuanCommandFactory; friend class ::Kleo::AssuanCommand; AssuanServerConnection *const q; public: Private(assuan_fd_t fd_, const std::vector< std::shared_ptr > &factories_, AssuanServerConnection *qq); ~Private() override; Q_SIGNALS: void startKeyManager(); public Q_SLOTS: void slotReadActivity(int) { Q_ASSERT(ctx); int done = false; if (const int err = assuan_process_next(ctx.get(), &done) || done) { //if ( err == -1 || gpg_err_code(err) == GPG_ERR_EOF ) { topHalfDeletion(); if (nohupedCommands.empty()) { bottomHalfDeletion(); } //} else { //assuan_process_done( ctx.get(), err ); //return; //} } } int startCommandBottomHalf(); private: void nohupDone(AssuanCommand *cmd) { const auto it = std::find_if(nohupedCommands.begin(), nohupedCommands.end(), [cmd](const std::shared_ptr &other) { return other.get() == cmd; }); Q_ASSERT(it != nohupedCommands.end()); nohupedCommands.erase(it); if (nohupedCommands.empty() && closed) { bottomHalfDeletion(); } } void commandDone(AssuanCommand *cmd) { if (!cmd || cmd != currentCommand.get()) { return; } currentCommand.reset(); } void topHalfDeletion() { if (currentCommand) { currentCommand->canceled(); } if (fd != ASSUAN_INVALID_FD) { #if defined(Q_OS_WIN) CloseHandle(fd); #else ::close(fd); #endif } notifiers.clear(); closed = true; } void bottomHalfDeletion() { if (sessionId) { SessionDataHandler::instance()->exitSession(sessionId); } cleanup(); const QPointer that = this; Q_EMIT q->closed(q); if (that) { // still there q->deleteLater(); } } private: static gpg_error_t reset_handler(assuan_context_t ctx_, char *) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); conn.reset(); return 0; } static gpg_error_t option_handler(assuan_context_t ctx_, const char *key, const char *value) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); if (key && key[0] == '-' && key[1] == '-') { key += 2; // skip "--" } conn.options[key] = QString::fromUtf8(value); return 0; //return gpg_error( GPG_ERR_UNKNOWN_OPTION ); } static gpg_error_t session_handler(assuan_context_t ctx_, char *line) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); const QString str = QString::fromUtf8(line); static const QRegularExpression rx(QRegularExpression::anchoredPattern(uR"((\d+)(?:\s+(.*))?)")); const QRegularExpressionMatch match = rx.match(str); if (!match.hasMatch()) { static const QString errorString = i18n("Parse error"); return assuan_process_done_msg(ctx_, gpg_error(GPG_ERR_ASS_SYNTAX), errorString); } bool ok = false; if (const qulonglong id = match.captured(1).toULongLong(&ok)) { if (ok && id <= std::numeric_limits::max()) { SessionDataHandler::instance()->enterSession(id); conn.sessionId = id; } else { static const QString errorString = i18n("Parse error: numeric session id too large"); return assuan_process_done_msg(ctx_, gpg_error(GPG_ERR_ASS_SYNTAX), errorString); } } const QString cap2 = match.captured(2); if (!cap2.isEmpty()) { conn.sessionTitle = cap2; } qCDebug(KLEOPATRA_LOG) << "session_handler: " << "id=" << static_cast(conn.sessionId) << ", title=" << qPrintable(conn.sessionTitle); return assuan_process_done(ctx_, 0); } static gpg_error_t capabilities_handler(assuan_context_t ctx_, char *line) { if (!QByteArray(line).trimmed().isEmpty()) { static const QString errorString = i18n("CAPABILITIES does not take arguments"); return assuan_process_done_msg(ctx_, gpg_error(GPG_ERR_ASS_PARAMETER), errorString); } static const char capabilities[] = "SENDER=info\n" "RECIPIENT=info\n" "SESSION\n" ; return assuan_process_done(ctx_, assuan_send_data(ctx_, capabilities, sizeof capabilities - 1)); } static gpg_error_t getinfo_handler(assuan_context_t ctx_, char *line) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); if (qstrcmp(line, "version") == 0) { static const char version[] = "Kleopatra " KLEOPATRA_VERSION_STRING; return assuan_process_done(ctx_, assuan_send_data(ctx_, version, sizeof version - 1)); } QByteArray ba; if (qstrcmp(line, "pid") == 0) { ba = QByteArray::number(QCoreApplication::applicationPid()); } else if (qstrcmp(line, "options") == 0) { ba = conn.dumpOptions(); } else if (qstrcmp(line, "x-mementos") == 0) { ba = conn.dumpMementos(); } else if (qstrcmp(line, "senders") == 0) { ba = conn.dumpSenders(); } else if (qstrcmp(line, "recipients") == 0) { ba = conn.dumpRecipients(); } else if (qstrcmp(line, "x-files") == 0) { ba = conn.dumpFiles(); } else { static const QString errorString = i18n("Unknown value for WHAT"); return assuan_process_done_msg(ctx_, gpg_error(GPG_ERR_ASS_PARAMETER), errorString); } return assuan_process_done(ctx_, assuan_send_data(ctx_, ba.constData(), ba.size())); } static gpg_error_t start_keymanager_handler(assuan_context_t ctx_, char *line) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); if (line && *line) { static const QString errorString = i18n("START_KEYMANAGER does not take arguments"); return assuan_process_done_msg(ctx_, gpg_error(GPG_ERR_ASS_PARAMETER), errorString); } Q_EMIT conn.q->startKeyManagerRequested(); return assuan_process_done(ctx_, 0); } static gpg_error_t start_confdialog_handler(assuan_context_t ctx_, char *line) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); if (line && *line) { static const QString errorString = i18n("START_CONFDIALOG does not take arguments"); return assuan_process_done_msg(ctx_, gpg_error(GPG_ERR_ASS_PARAMETER), errorString); } Q_EMIT conn.q->startConfigDialogRequested(); return assuan_process_done(ctx_, 0); } template struct Input_or_Output : std::conditional {}; // format: TAG (FD|FD=\d+|FILE=...) template static gpg_error_t IO_handler(assuan_context_t ctx_, char *line_, T_memptr which) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); char *binOpt = strstr(line_, "--binary"); if (binOpt && !in) { /* Note there is also --armor and --base64 allowed but we don't need * to parse those because they are default. * We remove it here so that it is not parsed as an Option.*/ memset(binOpt, ' ', 8); } try { /*const*/ std::map options = upcase_option("FD", upcase_option("FILE", parse_commandline(line_))); if (options.size() < 1 || options.size() > 2) { throw gpg_error(GPG_ERR_ASS_SYNTAX); } std::shared_ptr< typename Input_or_Output::type > io; if (options.count("FD")) { if (options.count("FILE")) { throw gpg_error(GPG_ERR_CONFLICT); } assuan_fd_t fd = ASSUAN_INVALID_FD; const std::string fdstr = options["FD"]; if (fdstr.empty()) { if (const gpg_error_t err = assuan_receivefd(conn.ctx.get(), &fd)) { throw err; } } else { #if defined(Q_OS_WIN) fd = (assuan_fd_t)std::stoi(fdstr); #else fd = std::stoi(fdstr); #endif } io = Input_or_Output::type::createFromPipeDevice(fd, in ? i18n("Message #%1", (conn.*which).size() + 1) : QString()); options.erase("FD"); } else if (options.count("FILE")) { if (options.count("FD")) { throw gpg_error(GPG_ERR_CONFLICT); } const QString filePath = QFile::decodeName(options["FILE"].c_str()); if (filePath.isEmpty()) { throw Exception(gpg_error(GPG_ERR_ASS_SYNTAX), i18n("Empty file path")); } const QFileInfo fi(filePath); if (!fi.isAbsolute()) { throw Exception(gpg_error(GPG_ERR_INV_ARG), i18n("Only absolute file paths are allowed")); } if (!fi.isFile()) { throw Exception(gpg_error(GPG_ERR_INV_ARG), i18n("Only files are allowed in INPUT/OUTPUT FILE")); } else { io = Input_or_Output::type::createFromFile(fi.absoluteFilePath(), true); } options.erase("FILE"); } else { throw gpg_error(GPG_ERR_ASS_PARAMETER); } if (options.size()) { throw gpg_error(GPG_ERR_UNKNOWN_OPTION); } (conn.*which).push_back(io); if (binOpt && !in) { auto out = reinterpret_cast (io.get()); out->setBinaryOpt(true); qCDebug(KLEOPATRA_LOG) << "Configured output for binary data"; } qCDebug(KLEOPATRA_LOG) << "AssuanServerConnection: added" << io->label(); return assuan_process_done(conn.ctx.get(), 0); } catch (const GpgME::Exception &e) { return assuan_process_done_msg(conn.ctx.get(), e.error().encodedError(), e.message().c_str()); } catch (const std::exception &) { return assuan_process_done(conn.ctx.get(), gpg_error(GPG_ERR_ASS_SYNTAX)); } catch (const gpg_error_t &e) { return assuan_process_done(conn.ctx.get(), e); } catch (...) { return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_UNEXPECTED), "unknown exception caught"); } } static gpg_error_t input_handler(assuan_context_t ctx, char *line) { return IO_handler(ctx, line, &Private::inputs); } static gpg_error_t output_handler(assuan_context_t ctx, char *line) { return IO_handler(ctx, line, &Private::outputs); } static gpg_error_t message_handler(assuan_context_t ctx, char *line) { return IO_handler(ctx, line, &Private::messages); } static gpg_error_t file_handler(assuan_context_t ctx_, char *line) { Q_ASSERT(assuan_get_pointer(ctx_)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx_)); try { const QFileInfo fi(QFile::decodeName(hexdecode(line).c_str())); if (!fi.isAbsolute()) { throw Exception(gpg_error(GPG_ERR_INV_ARG), i18n("Only absolute file paths are allowed")); } if (!fi.exists()) { throw gpg_error(GPG_ERR_ENOENT); } if (!fi.isReadable() || (fi.isDir() && !fi.isExecutable())) { throw gpg_error(GPG_ERR_EPERM); } conn.files.push_back(fi.absoluteFilePath()); return assuan_process_done(conn.ctx.get(), 0); } catch (const Exception &e) { return assuan_process_done_msg(conn.ctx.get(), e.error().encodedError(), e.message().toUtf8().constData()); } catch (const gpg_error_t &e) { return assuan_process_done(conn.ctx.get(), e); } catch (...) { return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_UNEXPECTED), i18n("unknown exception caught").toUtf8().constData()); } } static bool parse_informative(const char *&begin, GpgME::Protocol &protocol) { protocol = GpgME::UnknownProtocol; bool informative = false; const char *pos = begin; while (true) { while (*pos == ' ' || *pos == '\t') { ++pos; } if (qstrnicmp(pos, "--info", strlen("--info")) == 0) { informative = true; pos += strlen("--info"); if (*pos == '=') { ++pos; break; } } else if (qstrnicmp(pos, "--protocol=", strlen("--protocol=")) == 0) { pos += strlen("--protocol="); if (qstrnicmp(pos, "OpenPGP", strlen("OpenPGP")) == 0) { protocol = GpgME::OpenPGP; pos += strlen("OpenPGP"); } else if (qstrnicmp(pos, "CMS", strlen("CMS")) == 0) { protocol = GpgME::CMS; pos += strlen("CMS"); } else { ; } } else if (qstrncmp(pos, "-- ", strlen("-- ")) == 0) { pos += 3; while (*pos == ' ' || *pos == '\t') { ++pos; } break; } else { break; } } begin = pos; return informative; } template static gpg_error_t recipient_sender_handler(T_memptr mp, T_memptr2 info, assuan_context_t ctx, char *line, bool sender = false) { Q_ASSERT(assuan_get_pointer(ctx)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx)); if (!line || !*line) { return assuan_process_done(conn.ctx.get(), gpg_error(GPG_ERR_INV_ARG)); } const char *begin = line; const char *const end = begin + qstrlen(line); GpgME::Protocol proto = GpgME::UnknownProtocol; const bool informative = parse_informative(begin, proto); if (!(conn.*mp).empty() && informative != (conn.*info)) return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_CONFLICT), i18n("Cannot mix --info with non-info SENDER or RECIPIENT").toUtf8().constData()); KMime::Types::Mailbox mb; if (!KMime::HeaderParsing::parseMailbox(begin, end, mb)) return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_INV_ARG), i18n("Argument is not a valid RFC-2822 mailbox").toUtf8().constData()); if (begin != end) return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_INV_ARG), i18n("Garbage after valid RFC-2822 mailbox detected").toUtf8().constData()); (conn.*info) = informative; (conn.*mp).push_back(mb); const QString email = mb.addrSpec().asString(); (void)assuan_write_line(conn.ctx.get(), qPrintable(QString::asprintf("# ok, parsed as \"%s\"", qPrintable(email)))); if (sender && !informative) { return AssuanCommandFactory::_handle(conn.ctx.get(), line, "PREP_SIGN"); } else { return assuan_process_done(ctx, 0); } } static gpg_error_t recipient_handler(assuan_context_t ctx, char *line) { return recipient_sender_handler(&Private::recipients, &Private::informativeRecipients, ctx, line); } static gpg_error_t sender_handler(assuan_context_t ctx, char *line) { return recipient_sender_handler(&Private::senders, &Private::informativeSenders, ctx, line, true); } QByteArray dumpOptions() const { QByteArray result; for (auto it = options.begin(), end = options.end(); it != end; ++it) { result += it->first.c_str() + it->second.toString().toUtf8() + '\n'; } return result; } static QByteArray dumpStringList(const QStringList &sl) { return sl.join(QLatin1Char('\n')).toUtf8(); } template static QByteArray dumpStringList(const T_container &c) { QStringList sl; std::copy(c.begin(), c.end(), std::back_inserter(sl)); return dumpStringList(sl); } template static QByteArray dumpMailboxes(const T_container &c) { QStringList sl; std::transform(c.begin(), c.end(), std::back_inserter(sl), [](typename T_container::const_reference val) { return val.prettyAddress(); }); return dumpStringList(sl); } QByteArray dumpSenders() const { return dumpMailboxes(senders); } QByteArray dumpRecipients() const { return dumpMailboxes(recipients); } QByteArray dumpMementos() const { QByteArray result; for (auto it = mementos.begin(), end = mementos.end(); it != end; ++it) { char buf[2 + 2 * sizeof(void *) + 2]; sprintf(buf, "0x%p\n", (void *)it->second.get()); buf[sizeof(buf) - 1] = '\0'; result += it->first + QByteArray::fromRawData(buf, sizeof buf); } return result; } QByteArray dumpFiles() const { QStringList rv; rv.reserve(files.size()); std::copy(files.cbegin(), files.cend(), std::back_inserter(rv)); return dumpStringList(rv); } void cleanup(); void reset() { options.clear(); senders.clear(); informativeSenders = false; recipients.clear(); informativeRecipients = false; sessionTitle.clear(); sessionId = 0; mementos.clear(); files.clear(); std::for_each(inputs.begin(), inputs.end(), std::mem_fn(&Input::finalize)); inputs.clear(); std::for_each(outputs.begin(), outputs.end(), std::mem_fn(&Output::finalize)); outputs.clear(); std::for_each(messages.begin(), messages.end(), std::mem_fn(&Input::finalize)); messages.clear(); bias = GpgME::UnknownProtocol; } assuan_fd_t fd; AssuanContext ctx; bool closed : 1; bool cryptoCommandsEnabled : 1; bool commandWaitingForCryptoCommandsEnabled : 1; bool currentCommandIsNohup : 1; bool informativeSenders; // address taken, so no : 1 bool informativeRecipients; // address taken, so no : 1 GpgME::Protocol bias; QString sessionTitle; unsigned int sessionId; std::vector< std::shared_ptr > notifiers; std::vector< std::shared_ptr > factories; // sorted: _detail::ByName std::shared_ptr currentCommand; std::vector< std::shared_ptr > nohupedCommands; std::map options; std::vector senders, recipients; std::vector< std::shared_ptr > inputs, messages; std::vector< std::shared_ptr > outputs; std::vector files; std::map< QByteArray, std::shared_ptr > mementos; }; void AssuanServerConnection::Private::cleanup() { Q_ASSERT(nohupedCommands.empty()); reset(); currentCommand.reset(); currentCommandIsNohup = false; commandWaitingForCryptoCommandsEnabled = false; notifiers.clear(); ctx.reset(); fd = ASSUAN_INVALID_FD; } AssuanServerConnection::Private::Private(assuan_fd_t fd_, const std::vector< std::shared_ptr > &factories_, AssuanServerConnection *qq) : QObject(), q(qq), fd(fd_), closed(false), cryptoCommandsEnabled(false), commandWaitingForCryptoCommandsEnabled(false), currentCommandIsNohup(false), informativeSenders(false), informativeRecipients(false), bias(GpgME::UnknownProtocol), sessionId(0), factories(factories_) { #ifdef __GLIBCXX__ Q_ASSERT(__gnu_cxx::is_sorted(factories_.begin(), factories_.end(), _detail::ByName())); #endif if (fd == ASSUAN_INVALID_FD) { throw Exception(gpg_error(GPG_ERR_INV_ARG), "pre-assuan_init_socket_server_ext"); } { assuan_context_t naked_ctx = nullptr; if (const gpg_error_t err = assuan_new(&naked_ctx)) { throw Exception(err, "assuan_new"); } ctx.reset(naked_ctx); } if (const gpg_error_t err = assuan_init_socket_server(ctx.get(), fd, INIT_SOCKET_FLAGS)) throw Exception(err, "assuan_init_socket_server_ext"); // for callbacks, associate the context with this connection: assuan_set_pointer(ctx.get(), this); FILE *const logFile = Log::instance()->logFile(); assuan_set_log_stream(ctx.get(), logFile ? logFile : stderr); // register FDs with the event loop: assuan_fd_t fds[MAX_ACTIVE_FDS]; const int numFDs = assuan_get_active_fds(ctx.get(), FOR_READING, fds, MAX_ACTIVE_FDS); Q_ASSERT(numFDs != -1); // == 1 if (!numFDs || fds[0] != fd) { const std::shared_ptr sn(new QSocketNotifier((intptr_t)fd, QSocketNotifier::Read), std::mem_fn(&QObject::deleteLater)); connect(sn.get(), &QSocketNotifier::activated, this, &Private::slotReadActivity); notifiers.push_back(sn); } notifiers.reserve(notifiers.size() + numFDs); for (int i = 0; i < numFDs; ++i) { const std::shared_ptr sn(new QSocketNotifier((intptr_t)fds[i], QSocketNotifier::Read), std::mem_fn(&QObject::deleteLater)); connect(sn.get(), &QSocketNotifier::activated, this, &Private::slotReadActivity); notifiers.push_back(sn); } // register our INPUT/OUTPUT/MESSGAE/FILE handlers: if (const gpg_error_t err = assuan_register_command(ctx.get(), "INPUT", input_handler, "")) throw Exception(err, "register \"INPUT\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "MESSAGE", message_handler, "")) throw Exception(err, "register \"MESSAGE\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "OUTPUT", output_handler, "")) throw Exception(err, "register \"OUTPUT\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "FILE", file_handler, "")) throw Exception(err, "register \"FILE\" handler"); // register user-defined commands: for (std::shared_ptr fac : std::as_const(factories)) if (const gpg_error_t err = assuan_register_command(ctx.get(), fac->name(), fac->_handler(), "")) throw Exception(err, std::string("register \"") + fac->name() + "\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "GETINFO", getinfo_handler, "")) throw Exception(err, "register \"GETINFO\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "START_KEYMANAGER", start_keymanager_handler, "")) throw Exception(err, "register \"START_KEYMANAGER\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "START_CONFDIALOG", start_confdialog_handler, "")) throw Exception(err, "register \"START_CONFDIALOG\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "RECIPIENT", recipient_handler, "")) throw Exception(err, "register \"RECIPIENT\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "SENDER", sender_handler, "")) throw Exception(err, "register \"SENDER\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "SESSION", session_handler, "")) throw Exception(err, "register \"SESSION\" handler"); if (const gpg_error_t err = assuan_register_command(ctx.get(), "CAPABILITIES", capabilities_handler, "")) throw Exception(err, "register \"CAPABILITIES\" handler"); assuan_set_hello_line(ctx.get(), "GPG UI server (Kleopatra/" KLEOPATRA_VERSION_STRING ") ready to serve"); //assuan_set_hello_line( ctx.get(), GPG UI server (qApp->applicationName() + " v" + kapp->applicationVersion() + "ready to serve" ) // some notifiers we're interested in: if (const gpg_error_t err = assuan_register_reset_notify(ctx.get(), reset_handler)) { throw Exception(err, "register reset notify"); } if (const gpg_error_t err = assuan_register_option_handler(ctx.get(), option_handler)) { throw Exception(err, "register option handler"); } // and last, we need to call assuan_accept, which doesn't block // (d/t INIT_SOCKET_FLAGS), but performs vital connection // establishing handling: if (const gpg_error_t err = assuan_accept(ctx.get())) { throw Exception(err, "assuan_accept"); } } AssuanServerConnection::Private::~Private() { cleanup(); } AssuanServerConnection::AssuanServerConnection(assuan_fd_t fd, const std::vector< std::shared_ptr > &factories, QObject *p) : QObject(p), d(new Private(fd, factories, this)) { } AssuanServerConnection::~AssuanServerConnection() {} void AssuanServerConnection::enableCryptoCommands(bool on) { if (on == d->cryptoCommandsEnabled) { return; } d->cryptoCommandsEnabled = on; if (d->commandWaitingForCryptoCommandsEnabled) { QTimer::singleShot(0, d.get(), &Private::startCommandBottomHalf); } } // // // AssuanCommand: // // namespace Kleo { class InquiryHandler : public QObject { Q_OBJECT public: explicit InquiryHandler(const char *keyword_, QObject *p = nullptr) : QObject(p), keyword(keyword_) { } static gpg_error_t handler(void *cb_data, gpg_error_t rc, unsigned char *buffer, size_t buflen) { Q_ASSERT(cb_data); auto this_ = static_cast(cb_data); Q_EMIT this_->signal(rc, QByteArray::fromRawData(reinterpret_cast(buffer), buflen), this_->keyword); std::free(buffer); delete this_; return 0; } private: const char *keyword; Q_SIGNALS: void signal(int rc, const QByteArray &data, const QByteArray &keyword); }; } // namespace Kleo class AssuanCommand::Private { public: Private() : informativeRecipients(false), informativeSenders(false), bias(GpgME::UnknownProtocol), done(false), nohup(false) { } std::map options; std::vector< std::shared_ptr > inputs, messages; std::vector< std::shared_ptr > outputs; std::vector files; std::vector recipients, senders; bool informativeRecipients, informativeSenders; GpgME::Protocol bias; QString sessionTitle; unsigned int sessionId; QByteArray utf8ErrorKeepAlive; AssuanContext ctx; bool done; bool nohup; }; AssuanCommand::AssuanCommand() : d(new Private) { } AssuanCommand::~AssuanCommand() { } int AssuanCommand::start() { try { if (const int err = doStart()) if (!d->done) { done(err); } return 0; } catch (const Exception &e) { if (!d->done) { done(e.error_code(), e.message()); } return 0; } catch (const GpgME::Exception &e) { if (!d->done) { done(e.error(), QString::fromLocal8Bit(e.message().c_str())); } return 0; } catch (const std::exception &e) { if (!d->done) { done(makeError(GPG_ERR_INTERNAL), i18n("Caught unexpected exception: %1", QString::fromLocal8Bit(e.what()))); } return 0; } catch (...) { if (!d->done) { done(makeError(GPG_ERR_INTERNAL), i18n("Caught unknown exception - please report this error to the developers.")); } return 0; } } void AssuanCommand::canceled() { d->done = true; doCanceled(); } // static int AssuanCommand::makeError(int code) { return makeGnuPGError(code); } bool AssuanCommand::hasOption(const char *opt) const { return d->options.count(opt); } QVariant AssuanCommand::option(const char *opt) const { const auto it = d->options.find(opt); if (it == d->options.end()) { return QVariant(); } else { return it->second; } } const std::map &AssuanCommand::options() const { return d->options; } namespace { template std::vector keys(const std::map &map) { std::vector result; result.resize(map.size()); for (typename std::map::const_iterator it = map.begin(), end = map.end(); it != end; ++it) { result.push_back(it->first); } return result; } } const std::map< QByteArray, std::shared_ptr > &AssuanCommand::mementos() const { // oh, hack :( Q_ASSERT(assuan_get_pointer(d->ctx.get())); const AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(d->ctx.get())); return conn.mementos; } bool AssuanCommand::hasMemento(const QByteArray &tag) const { if (const unsigned int id = sessionId()) { return SessionDataHandler::instance()->sessionData(id)->mementos.count(tag) || mementos().count(tag); } else { return mementos().count(tag); } } std::shared_ptr AssuanCommand::memento(const QByteArray &tag) const { if (const unsigned int id = sessionId()) { const std::shared_ptr sdh = SessionDataHandler::instance(); const std::shared_ptr sd = sdh->sessionData(id); const auto it = sd->mementos.find(tag); if (it != sd->mementos.end()) { return it->second; } } const auto it = mementos().find(tag); if (it == mementos().end()) { return std::shared_ptr(); } else { return it->second; } } QByteArray AssuanCommand::registerMemento(const std::shared_ptr &mem) { const QByteArray tag = QByteArray::number(reinterpret_cast(mem.get()), 36); return registerMemento(tag, mem); } QByteArray AssuanCommand::registerMemento(const QByteArray &tag, const std::shared_ptr &mem) { // oh, hack :( Q_ASSERT(assuan_get_pointer(d->ctx.get())); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(d->ctx.get())); if (const unsigned int id = sessionId()) { SessionDataHandler::instance()->sessionData(id)->mementos[tag] = mem; } else { conn.mementos[tag] = mem; } return tag; } void AssuanCommand::removeMemento(const QByteArray &tag) { // oh, hack :( Q_ASSERT(assuan_get_pointer(d->ctx.get())); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(d->ctx.get())); conn.mementos.erase(tag); if (const unsigned int id = sessionId()) { SessionDataHandler::instance()->sessionData(id)->mementos.erase(tag); } } const std::vector< std::shared_ptr > &AssuanCommand::inputs() const { return d->inputs; } const std::vector< std::shared_ptr > &AssuanCommand::messages() const { return d->messages; } const std::vector< std::shared_ptr > &AssuanCommand::outputs() const { return d->outputs; } QStringList AssuanCommand::fileNames() const { QStringList rv; rv.reserve(d->files.size()); std::copy(d->files.cbegin(), d->files.cend(), std::back_inserter(rv)); return rv; } unsigned int AssuanCommand::numFiles() const { return d->files.size(); } void AssuanCommand::sendStatus(const char *keyword, const QString &text) { sendStatusEncoded(keyword, text.toUtf8().constData()); } void AssuanCommand::sendStatusEncoded(const char *keyword, const std::string &text) { if (d->nohup) { return; } if (const int err = assuan_write_status(d->ctx.get(), keyword, text.c_str())) { throw Exception(err, i18n("Cannot send \"%1\" status", QString::fromLatin1(keyword))); } } void AssuanCommand::sendData(const QByteArray &data, bool moreToCome) { if (d->nohup) { return; } if (const gpg_error_t err = assuan_send_data(d->ctx.get(), data.constData(), data.size())) { throw Exception(err, i18n("Cannot send data")); } if (!moreToCome) if (const gpg_error_t err = assuan_send_data(d->ctx.get(), nullptr, 0)) { // flush throw Exception(err, i18n("Cannot flush data")); } } int AssuanCommand::inquire(const char *keyword, QObject *receiver, const char *slot, unsigned int maxSize) { Q_ASSERT(keyword); Q_ASSERT(receiver); Q_ASSERT(slot); if (d->nohup) { return makeError(GPG_ERR_INV_OP); } std::unique_ptr ih(new InquiryHandler(keyword, receiver)); receiver->connect(ih.get(), SIGNAL(signal(int,QByteArray,QByteArray)), slot); if (const gpg_error_t err = assuan_inquire_ext(d->ctx.get(), keyword, maxSize, InquiryHandler::handler, ih.get())) { return err; } ih.release(); return 0; } void AssuanCommand::done(const GpgME::Error &err, const QString &details) { if (d->ctx && !d->done && !details.isEmpty()) { qCDebug(KLEOPATRA_LOG) << "Error: " << details; d->utf8ErrorKeepAlive = details.toUtf8(); if (!d->nohup) { assuan_set_error(d->ctx.get(), err.encodedError(), d->utf8ErrorKeepAlive.constData()); } } done(err); } void AssuanCommand::done(const GpgME::Error &err) { if (!d->ctx) { - qCDebug(KLEOPATRA_LOG) << err.asString() << ": called with NULL ctx."; + qCDebug(KLEOPATRA_LOG) << Formatting::errorAsString(err) << ": called with NULL ctx."; return; } if (d->done) { - qCDebug(KLEOPATRA_LOG) << err.asString() << ": called twice!"; + qCDebug(KLEOPATRA_LOG) << Formatting::errorAsString(err) << ": called twice!"; return; } d->done = true; std::for_each(d->messages.begin(), d->messages.end(), std::mem_fn(&Input::finalize)); std::for_each(d->inputs.begin(), d->inputs.end(), std::mem_fn(&Input::finalize)); std::for_each(d->outputs.begin(), d->outputs.end(), std::mem_fn(&Output::finalize)); d->messages.clear(); d->inputs.clear(); d->outputs.clear(); d->files.clear(); // oh, hack :( Q_ASSERT(assuan_get_pointer(d->ctx.get())); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(d->ctx.get())); if (d->nohup) { conn.nohupDone(this); return; } const gpg_error_t rc = assuan_process_done(d->ctx.get(), err.encodedError()); if (gpg_err_code(rc) != GPG_ERR_NO_ERROR) qFatal("AssuanCommand::done: assuan_process_done returned error %d (%s)", static_cast(rc), gpg_strerror(rc)); d->utf8ErrorKeepAlive.clear(); conn.commandDone(this); } void AssuanCommand::setNohup(bool nohup) { d->nohup = nohup; } bool AssuanCommand::isNohup() const { return d->nohup; } bool AssuanCommand::isDone() const { return d->done; } QString AssuanCommand::sessionTitle() const { return d->sessionTitle; } unsigned int AssuanCommand::sessionId() const { return d->sessionId; } bool AssuanCommand::informativeSenders() const { return d->informativeSenders; } bool AssuanCommand::informativeRecipients() const { return d->informativeRecipients; } const std::vector &AssuanCommand::recipients() const { return d->recipients; } const std::vector &AssuanCommand::senders() const { return d->senders; } gpg_error_t AssuanCommandFactory::_handle(assuan_context_t ctx, char *line, const char *commandName) { Q_ASSERT(assuan_get_pointer(ctx)); AssuanServerConnection::Private &conn = *static_cast(assuan_get_pointer(ctx)); try { const auto it = std::lower_bound(conn.factories.begin(), conn.factories.end(), commandName, _detail::ByName()); kleo_assert(it != conn.factories.end()); kleo_assert(*it); kleo_assert(qstricmp((*it)->name(), commandName) == 0); const std::shared_ptr cmd = (*it)->create(); kleo_assert(cmd); cmd->d->ctx = conn.ctx; cmd->d->options = conn.options; cmd->d->inputs.swap(conn.inputs); kleo_assert(conn.inputs.empty()); cmd->d->messages.swap(conn.messages); kleo_assert(conn.messages.empty()); cmd->d->outputs.swap(conn.outputs); kleo_assert(conn.outputs.empty()); cmd->d->files.swap(conn.files); kleo_assert(conn.files.empty()); cmd->d->senders.swap(conn.senders); kleo_assert(conn.senders.empty()); cmd->d->recipients.swap(conn.recipients); kleo_assert(conn.recipients.empty()); cmd->d->informativeRecipients = conn.informativeRecipients; cmd->d->informativeSenders = conn.informativeSenders; cmd->d->bias = conn.bias; cmd->d->sessionTitle = conn.sessionTitle; cmd->d->sessionId = conn.sessionId; const std::map cmdline_options = parse_commandline(line); for (auto it = cmdline_options.begin(), end = cmdline_options.end(); it != end; ++it) { cmd->d->options[it->first] = QString::fromUtf8(it->second.c_str()); } bool nohup = false; if (cmd->d->options.count("nohup")) { if (!cmd->d->options["nohup"].toString().isEmpty()) { return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_ASS_PARAMETER), "--nohup takes no argument"); } nohup = true; cmd->d->options.erase("nohup"); } conn.currentCommand = cmd; conn.currentCommandIsNohup = nohup; QTimer::singleShot(0, &conn, &AssuanServerConnection::Private::startCommandBottomHalf); return 0; } catch (const Exception &e) { return assuan_process_done_msg(conn.ctx.get(), e.error_code(), e.message()); } catch (const std::exception &e) { return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_UNEXPECTED), e.what()); } catch (...) { return assuan_process_done_msg(conn.ctx.get(), gpg_error(GPG_ERR_UNEXPECTED), i18n("Caught unknown exception")); } } int AssuanServerConnection::Private::startCommandBottomHalf() { commandWaitingForCryptoCommandsEnabled = currentCommand && !cryptoCommandsEnabled; if (!cryptoCommandsEnabled) { return 0; } const std::shared_ptr cmd = currentCommand; if (!cmd) { return 0; } currentCommand.reset(); const bool nohup = currentCommandIsNohup; currentCommandIsNohup = false; try { if (const int err = cmd->start()) { if (cmd->isDone()) { return err; } else { return assuan_process_done(ctx.get(), err); } } if (cmd->isDone()) { return 0; } if (nohup) { cmd->setNohup(true); nohupedCommands.push_back(cmd); return assuan_process_done_msg(ctx.get(), 0, "Command put in the background to continue executing after connection end."); } else { currentCommand = cmd; return 0; } } catch (const Exception &e) { return assuan_process_done_msg(ctx.get(), e.error_code(), e.message()); } catch (const std::exception &e) { return assuan_process_done_msg(ctx.get(), gpg_error(GPG_ERR_UNEXPECTED), e.what()); } catch (...) { return assuan_process_done_msg(ctx.get(), gpg_error(GPG_ERR_UNEXPECTED), i18n("Caught unknown exception")); } } // // // AssuanCommand convenience methods // // /*! Checks the \c --mode parameter. \returns The parameter as an AssuanCommand::Mode enum value. If no \c --mode was given, or it's value wasn't recognized, throws an Kleo::Exception. */ AssuanCommand::Mode AssuanCommand::checkMode() const { if (!hasOption("mode")) { throw Exception(makeError(GPG_ERR_MISSING_VALUE), i18n("Required --mode option missing")); } const QString modeString = option("mode").toString().toLower(); if (modeString == QLatin1String("filemanager")) { return FileManager; } if (modeString == QLatin1String("email")) { return EMail; } throw Exception(makeError(GPG_ERR_INV_ARG), i18n("invalid mode: \"%1\"", modeString)); } /*! Checks the \c --protocol parameter. \returns The parameter as a GpgME::Protocol enum value. If \c --protocol was given, but has an invalid value, throws an Kleo::Exception. If no \c --protocol was given, checks the connection bias, if available, otherwise, in FileManager mode, returns GpgME::UnknownProtocol, but if \a mode == \c EMail, throws an Kleo::Exception instead. */ GpgME::Protocol AssuanCommand::checkProtocol(Mode mode, int options) const { if (!hasOption("protocol")) if (d->bias != GpgME::UnknownProtocol) { return d->bias; } else if (mode == AssuanCommand::EMail && (options & AllowProtocolMissing) == 0) { throw Exception(makeError(GPG_ERR_MISSING_VALUE), i18n("Required --protocol option missing")); } else { return GpgME::UnknownProtocol; } else if (mode == AssuanCommand::FileManager) { throw Exception(makeError(GPG_ERR_INV_FLAG), i18n("--protocol is not allowed here")); } const QString protocolString = option("protocol").toString().toLower(); if (protocolString == QLatin1String("openpgp")) { return GpgME::OpenPGP; } if (protocolString == QLatin1String("cms")) { return GpgME::CMS; } throw Exception(makeError(GPG_ERR_INV_ARG), i18n("invalid protocol \"%1\"", protocolString)); } void AssuanCommand::doApplyWindowID(QWidget *widget) const { if (!widget || !hasOption("window-id")) { return; } apply_window_id(widget, option("window-id").toString()); } WId AssuanCommand::parentWId() const { return wid_from_string(option("window-id").toString()); } #include "assuanserverconnection.moc" diff --git a/src/view/pgpcardwidget.cpp b/src/view/pgpcardwidget.cpp index 1b9f5c12c..bd83bb50e 100644 --- a/src/view/pgpcardwidget.cpp +++ b/src/view/pgpcardwidget.cpp @@ -1,580 +1,580 @@ /* view/pgpcardwiget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2020, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "pgpcardwidget.h" #include "openpgpkeycardwidget.h" #include "kleopatra_debug.h" #include "commands/createcsrforcardkeycommand.h" #include "commands/createopenpgpkeyfromcardkeyscommand.h" #include "commands/openpgpgeneratecardkeycommand.h" #include "smartcard/algorithminfo.h" #include "smartcard/openpgpcard.h" #include "smartcard/readerstatus.h" #include "dialogs/gencardkeydialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) static QDebug operator<<(QDebug s, const std::string &string) { return s << QString::fromStdString(string); } #endif namespace { class GenKeyThread: public QThread { Q_OBJECT public: explicit GenKeyThread(const GenCardKeyDialog::KeyParams ¶ms, const std::string &serial): mSerial(serial), mParams(params) { } GpgME::Error error() { return mErr; } std::string bkpFile() { return mBkpFile; } protected: void run() override { // the index of the curves in this list has to match the enum values // minus 1 of GpgGenCardKeyInteractor::Curve static const std::vector curves = { "curve25519", #if GPGMEPP_SUPPORTS_SET_CURVE "curve448", "nistp256", "nistp384", "nistp521", "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", "secp256k1", // keep it, even if we don't support it in Kleopatra #endif }; auto ei = std::make_unique(mSerial); if (mParams.algorithm.starts_with("rsa")) { ei->setAlgo(GpgME::GpgGenCardKeyInteractor::RSA); ei->setKeySize(QByteArray::fromStdString(mParams.algorithm.substr(3)).toInt()); } else { ei->setAlgo(GpgME::GpgGenCardKeyInteractor::ECC); const auto curveIt = std::find(curves.cbegin(), curves.cend(), mParams.algorithm); if (curveIt != curves.end()) { #if GPGMEPP_SUPPORTS_SET_CURVE ei->setCurve(static_cast(curveIt - curves.cbegin() + 1)); #endif } else { qCWarning(KLEOPATRA_LOG) << this << __func__ << "Invalid curve name:" << mParams.algorithm; mErr = GpgME::Error::fromCode(GPG_ERR_INV_VALUE); return; } } ei->setNameUtf8(mParams.name.toStdString()); ei->setEmailUtf8(mParams.email.toStdString()); ei->setDoBackup(mParams.backup); const auto ctx = std::shared_ptr (GpgME::Context::createForProtocol(GpgME::OpenPGP)); ctx->setFlag("extended-edit", "1"); // we want to be able to select all curves QGpgME::QByteArrayDataProvider dp; GpgME::Data data(&dp); mErr = ctx->cardEdit(GpgME::Key(), std::move(ei), data); mBkpFile = static_cast(ctx->lastCardEditInteractor())->backupFileName(); } private: GpgME::Error mErr; std::string mSerial; GenCardKeyDialog::KeyParams mParams; std::string mBkpFile; }; } // Namespace PGPCardWidget::PGPCardWidget(QWidget *parent): QWidget(parent), mSerialNumber(new QLabel(this)), mCardHolderLabel(new QLabel(this)), mVersionLabel(new QLabel(this)), mUrlLabel(new QLabel(this)), mCardIsEmpty(false) { // Set up the scroll area auto myLayout = new QVBoxLayout(this); myLayout->setContentsMargins(0, 0, 0, 0); auto area = new QScrollArea; area->setFrameShape(QFrame::NoFrame); area->setWidgetResizable(true); myLayout->addWidget(area); auto areaWidget = new QWidget; area->setWidget(areaWidget); auto areaVLay = new QVBoxLayout(areaWidget); auto cardInfoGrid = new QGridLayout; { int row = 0; // Version and Serialnumber cardInfoGrid->addWidget(mVersionLabel, row, 0, 1, 2); mVersionLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); row++; cardInfoGrid->addWidget(new QLabel(i18n("Serial number:")), row, 0); cardInfoGrid->addWidget(mSerialNumber, row, 1); mSerialNumber->setTextInteractionFlags(Qt::TextBrowserInteraction); row++; // Cardholder Row cardInfoGrid->addWidget(new QLabel(i18nc("The owner of a smartcard. GnuPG refers to this as cardholder.", "Cardholder:")), row, 0); cardInfoGrid->addWidget(mCardHolderLabel, row, 1); mCardHolderLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); { auto button = new QPushButton; button->setIcon(QIcon::fromTheme(QStringLiteral("cell_edit"))); button->setAccessibleName(i18nc("@action:button", "Edit")); button->setToolTip(i18n("Change")); cardInfoGrid->addWidget(button, row, 2); connect(button, &QPushButton::clicked, this, &PGPCardWidget::changeNameRequested); } row++; // URL Row cardInfoGrid->addWidget(new QLabel(i18nc("The URL under which a public key that " "corresponds to a smartcard can be downloaded", "Pubkey URL:")), row, 0); cardInfoGrid->addWidget(mUrlLabel, row, 1); mUrlLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); { auto button = new QPushButton; button->setIcon(QIcon::fromTheme(QStringLiteral("cell_edit"))); button->setAccessibleName(i18nc("@action:button", "Edit")); button->setToolTip(i18n("Change")); cardInfoGrid->addWidget(button, row, 2); connect(button, &QPushButton::clicked, this, &PGPCardWidget::changeUrlRequested); } cardInfoGrid->setColumnStretch(cardInfoGrid->columnCount(), 1); } areaVLay->addLayout(cardInfoGrid); areaVLay->addWidget(new KSeparator(Qt::Horizontal)); // The keys areaVLay->addWidget(new QLabel(QStringLiteral("%1").arg(i18n("Keys:")))); mKeysWidget = new OpenPGPKeyCardWidget{this}; areaVLay->addWidget(mKeysWidget); connect(mKeysWidget, &OpenPGPKeyCardWidget::createCSRRequested, this, &PGPCardWidget::createCSR); connect(mKeysWidget, &OpenPGPKeyCardWidget::generateKeyRequested, this, &PGPCardWidget::generateKey); areaVLay->addWidget(new KSeparator(Qt::Horizontal)); areaVLay->addWidget(new QLabel(QStringLiteral("%1").arg(i18n("Actions:")))); auto actionLayout = new QHBoxLayout; { auto generateButton = new QPushButton(i18n("Generate New Keys")); generateButton->setToolTip(i18n("Create a new primary key and generate subkeys on the card.")); actionLayout->addWidget(generateButton); connect(generateButton, &QPushButton::clicked, this, &PGPCardWidget::genkeyRequested); } { auto pinButton = new QPushButton(i18n("Change PIN")); pinButton->setToolTip(i18n("Change the PIN required for using the keys on the smartcard.")); actionLayout->addWidget(pinButton); connect(pinButton, &QPushButton::clicked, this, [this] () { doChangePin(OpenPGPCard::pinKeyRef()); }); } { auto unblockButton = new QPushButton(i18n("Unblock Card")); unblockButton->setToolTip(i18n("Unblock the smartcard and set a new PIN.")); actionLayout->addWidget(unblockButton); connect(unblockButton, &QPushButton::clicked, this, [this] () { doChangePin(OpenPGPCard::resetCodeKeyRef()); }); } { auto pukButton = new QPushButton(i18n("Change Admin PIN")); pukButton->setToolTip(i18n("Change the PIN required for administrative operations.")); actionLayout->addWidget(pukButton); connect(pukButton, &QPushButton::clicked, this, [this] () { doChangePin(OpenPGPCard::adminPinKeyRef()); }); } { auto resetCodeButton = new QPushButton(i18n("Change Reset Code")); resetCodeButton->setToolTip(i18n("Change the PIN required to unblock the smartcard and set a new PIN.")); actionLayout->addWidget(resetCodeButton); connect(resetCodeButton, &QPushButton::clicked, this, [this] () { doChangePin(OpenPGPCard::resetCodeKeyRef(), ChangePinCommand::ResetMode); }); } if (CreateOpenPGPKeyFromCardKeysCommand::isSupported()) { mKeyForCardKeysButton = new QPushButton(this); mKeyForCardKeysButton->setText(i18n("Create OpenPGP Key")); mKeyForCardKeysButton->setToolTip(i18n("Create an OpenPGP key for the keys stored on the card.")); actionLayout->addWidget(mKeyForCardKeysButton); connect(mKeyForCardKeysButton, &QPushButton::clicked, this, &PGPCardWidget::createKeyFromCardKeys); } actionLayout->addStretch(-1); areaVLay->addLayout(actionLayout); areaVLay->addStretch(1); } void PGPCardWidget::setCard(const OpenPGPCard *card) { const QString version = card->displayAppVersion(); mIs21 = card->appVersion() >= 0x0201; const QString manufacturer = QString::fromStdString(card->manufacturer()); const bool manufacturerIsUnknown = manufacturer.isEmpty() || manufacturer == QLatin1String("unknown"); mVersionLabel->setText(manufacturerIsUnknown ? i18nc("Placeholder is a version number", "Unknown OpenPGP v%1 card", version) : i18nc("First placeholder is manufacturer, second placeholder is a version number", "%1 OpenPGP v%2 card", manufacturer, version)); mSerialNumber->setText(card->displaySerialNumber()); mRealSerial = card->serialNumber(); const auto holder = card->cardHolder(); const auto url = QString::fromStdString(card->pubkeyUrl()); mCardHolderLabel->setText(holder.isEmpty() ? i18n("not set") : holder); mUrl = url; mUrlLabel->setText(url.isEmpty() ? i18n("not set") : QStringLiteral("%1").arg(url.toHtmlEscaped())); mUrlLabel->setOpenExternalLinks(true); mKeysWidget->update(card); mCardIsEmpty = card->keyFingerprint(OpenPGPCard::pgpSigKeyRef()).empty() && card->keyFingerprint(OpenPGPCard::pgpEncKeyRef()).empty() && card->keyFingerprint(OpenPGPCard::pgpAuthKeyRef()).empty(); if (mKeyForCardKeysButton) { mKeyForCardKeysButton->setEnabled(card->hasSigningKey() && card->hasEncryptionKey() && DeVSCompliance::algorithmIsCompliant(card->keyInfo(card->signingKeyRef()).algorithm) && DeVSCompliance::algorithmIsCompliant(card->keyInfo(card->encryptionKeyRef()).algorithm)); } } void PGPCardWidget::doChangePin(const std::string &keyRef, ChangePinCommand::ChangePinMode mode) { auto cmd = new ChangePinCommand(mRealSerial, OpenPGPCard::AppName, this); this->setEnabled(false); connect(cmd, &ChangePinCommand::finished, this, [this]() { this->setEnabled(true); }); cmd->setKeyRef(keyRef); cmd->setMode(mode); cmd->start(); } void PGPCardWidget::doGenKey(GenCardKeyDialog *dlg) { const GpgME::Error err = ReaderStatus::switchCardAndApp(mRealSerial, OpenPGPCard::AppName); if (err) { return; } const auto params = dlg->getKeyParams(); auto progress = new QProgressDialog(this, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::Dialog); progress->setAutoClose(true); progress->setMinimumDuration(0); progress->setMaximum(0); progress->setMinimum(0); progress->setModal(true); progress->setCancelButton(nullptr); progress->setWindowTitle(i18nc("@title:window", "Generating Keys")); progress->setLabel(new QLabel(i18n("This may take several minutes..."))); auto workerThread = new GenKeyThread(params, mRealSerial); connect(workerThread, &QThread::finished, this, [this, workerThread, progress] { progress->accept(); progress->deleteLater(); genKeyDone(workerThread->error(), workerThread->bkpFile()); delete workerThread; }); workerThread->start(); progress->exec(); } void PGPCardWidget::genKeyDone(const GpgME::Error &err, const std::string &backup) { if (err) { KMessageBox::error(this, i18nc("@info", - "Failed to generate new key: %1", QString::fromLatin1(err.asString()))); + "Failed to generate new key: %1", Formatting::errorAsString(err))); return; } if (err.isCanceled()) { return; } if (!backup.empty()) { const auto bkpFile = QString::fromStdString(backup); QFileInfo fi(bkpFile); const auto target = QFileDialog::getSaveFileName(this, i18n("Save backup of encryption key"), fi.fileName(), QStringLiteral("%1 (*.gpg)").arg(i18n("Backup Key"))); if (!target.isEmpty() && !QFile::copy(bkpFile, target)) { KMessageBox::error(this, i18nc("@info", "Failed to move backup. The backup key is still stored under: %1", bkpFile)); } else if (!target.isEmpty()) { QFile::remove(bkpFile); } } KMessageBox::information(this, i18nc("@info", "Successfully generated a new key for this card."), i18nc("@title", "Success")); ReaderStatus::mutableInstance()->updateStatus(); } void PGPCardWidget::genkeyRequested() { const auto pgpCard = ReaderStatus::instance()->getCard(mRealSerial); if (!pgpCard) { KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial))); return; } if (!mCardIsEmpty) { auto ret = KMessageBox::warningContinueCancel(this, i18n("The existing keys on this card will be deleted " "and replaced by new keys.") + QStringLiteral("

    ") + i18n("It will no longer be possible to decrypt past communication " "encrypted for the existing key."), i18n("Secret Key Deletion"), KStandardGuiItem::guiItem(KStandardGuiItem::Delete), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (ret != KMessageBox::Continue) { return; } } auto dlg = new GenCardKeyDialog(GenCardKeyDialog::AllKeyAttributes, this); #if GPGMEPP_SUPPORTS_SET_CURVE dlg->setSupportedAlgorithms(pgpCard->supportedAlgorithms(), pgpCard->defaultAlgorithm()); #else std::vector algos = { { "rsa2048", i18nc("@info", "RSA 2048") }, { "rsa3072", i18nc("@info", "RSA 3072") }, }; // There is probably a better way to check for capabilities if (mIs21) { algos.push_back({"rsa4096", i18nc("@info", "RSA 4096")}); } const auto supportedAlgos = pgpCard->supportedAlgorithms(); if (std::any_of(supportedAlgos.begin(), supportedAlgos.end(), [](const auto &algo) { return algo.id == "curve25519"; })) { algos.push_back({"curve25519", i18nc("@info", "ECC (Curve25519)")}); } dlg->setSupportedAlgorithms(algos, "rsa2048"); #endif connect(dlg, &QDialog::accepted, this, [this, dlg] () { doGenKey(dlg); dlg->deleteLater(); }); dlg->setModal(true); dlg->show(); } void PGPCardWidget::changeNameRequested() { QString text = mCardHolderLabel->text(); while (true) { bool ok = false; text = QInputDialog::getText(this, i18n("Change cardholder"), i18n("New name:"), QLineEdit::Normal, text, &ok, Qt::WindowFlags(), Qt::ImhLatinOnly); if (!ok) { return; } // Some additional restrictions imposed by gnupg if (text.contains(QLatin1Char('<'))) { KMessageBox::error(this, i18nc("@info", "The \"<\" character may not be used.")); continue; } if (text.contains(QLatin1String(" "))) { KMessageBox::error(this, i18nc("@info", "Double spaces are not allowed")); continue; } if (text.size() > 38) { KMessageBox::error(this, i18nc("@info", "The size of the name may not exceed 38 characters.")); } break; } auto parts = text.split(QLatin1Char(' ')); const auto lastName = parts.takeLast(); const QString formatted = lastName + QStringLiteral("<<") + parts.join(QLatin1Char('<')); const auto pgpCard = ReaderStatus::instance()->getCard(mRealSerial); if (!pgpCard) { KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial))); return; } const QByteArray command = QByteArrayLiteral("SCD SETATTR DISP-NAME ") + formatted.toUtf8(); ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, command, this, [this](const GpgME::Error &err) { changeNameResult(err); }); } void PGPCardWidget::changeNameResult(const GpgME::Error &err) { if (err) { KMessageBox::error(this, i18nc("@info", - "Name change failed: %1", QString::fromLatin1(err.asString()))); + "Name change failed: %1", Formatting::errorAsString(err))); return; } if (!err.isCanceled()) { KMessageBox::information(this, i18nc("@info", "Name successfully changed."), i18nc("@title", "Success")); ReaderStatus::mutableInstance()->updateStatus(); } } void PGPCardWidget::changeUrlRequested() { QString text = mUrl; while (true) { bool ok = false; text = QInputDialog::getText(this, i18n("Change the URL where the pubkey can be found"), i18n("New pubkey URL:"), QLineEdit::Normal, text, &ok, Qt::WindowFlags(), Qt::ImhLatinOnly); if (!ok) { return; } // Some additional restrictions imposed by gnupg if (text.size() > 254) { KMessageBox::error(this, i18nc("@info", "The size of the URL may not exceed 254 characters.")); } break; } const auto pgpCard = ReaderStatus::instance()->getCard(mRealSerial); if (!pgpCard) { KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial))); return; } const QByteArray command = QByteArrayLiteral("SCD SETATTR PUBKEY-URL ") + text.toUtf8(); ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, command, this, [this](const GpgME::Error &err) { changeUrlResult(err); }); } void PGPCardWidget::changeUrlResult(const GpgME::Error &err) { if (err) { KMessageBox::error(this, i18nc("@info", - "URL change failed: %1", QString::fromLatin1(err.asString()))); + "URL change failed: %1", Formatting::errorAsString(err))); return; } if (!err.isCanceled()) { KMessageBox::information(this, i18nc("@info", "URL successfully changed."), i18nc("@title", "Success")); ReaderStatus::mutableInstance()->updateStatus(); } } void PGPCardWidget::createKeyFromCardKeys() { auto cmd = new CreateOpenPGPKeyFromCardKeysCommand(mRealSerial, OpenPGPCard::AppName, this); this->setEnabled(false); connect(cmd, &CreateOpenPGPKeyFromCardKeysCommand::finished, this, [this]() { this->setEnabled(true); }); cmd->start(); } void PGPCardWidget::createCSR(const std::string &keyref) { auto cmd = new CreateCSRForCardKeyCommand(keyref, mRealSerial, OpenPGPCard::AppName, this); this->setEnabled(false); connect(cmd, &CreateCSRForCardKeyCommand::finished, this, [this]() { this->setEnabled(true); }); cmd->start(); } void PGPCardWidget::generateKey(const std::string &keyref) { auto cmd = new OpenPGPGenerateCardKeyCommand(keyref, mRealSerial, this); this->setEnabled(false); connect(cmd, &OpenPGPGenerateCardKeyCommand::finished, this, [this]() { this->setEnabled(true); }); cmd->start(); } #include "pgpcardwidget.moc"