diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 253f4423a..5862d50cf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,391 +1,397 @@ # SPDX-License-Identifier: CC0-1.0 # SPDX-FileCopyrightText: none # target_include_directories does not handle empty include paths include_directories( ${Boost_INCLUDE_DIRS} ${GPGME_INCLUDES} ) add_definitions(-DTRANSLATION_DOMAIN=\"libkleopatra6\") #add_definitions( -DQT_NO_CAST_FROM_ASCII ) #add_definitions( -DQT_NO_CAST_TO_ASCII ) kde_enable_exceptions() add_definitions( -DGPGMEPP_ERR_SOURCE_DEFAULT=13 ) # 13 is GPG_ERR_SOURCE_KLEO, even if gpg-error's too old to know about add_subdirectory( pics ) add_library(KPim6Libkleo) add_library(KPim6::Libkleo ALIAS KPim6Libkleo) ########### next target ############### target_sources(KPim6Libkleo PRIVATE kleo/auditlogentry.cpp kleo/auditlogentry.h kleo/checksumdefinition.cpp kleo/checksumdefinition.h kleo/debug.cpp kleo/debug.h kleo/defaultkeyfilter.cpp kleo/defaultkeyfilter.h kleo/defaultkeygenerationjob.cpp kleo/defaultkeygenerationjob.h kleo/docaction.cpp kleo/docaction.h kleo/dn.cpp kleo/dn.h kleo/enum.cpp kleo/enum.h kleo/expirychecker.cpp kleo/expirychecker.h kleo/expirycheckerconfig.cpp kleo/expirycheckerconfig.h kleo/expirycheckersettings.cpp kleo/expirycheckersettings.h kleo/kconfigbasedkeyfilter.cpp kleo/kconfigbasedkeyfilter.h kleo/keyfilter.h kleo/keyfiltermanager.cpp kleo/keyfiltermanager.h kleo/keygroup.cpp kleo/keygroup.h kleo/keygroupconfig.cpp kleo/keygroupconfig.h kleo/keygroupimportexport.cpp kleo/keygroupimportexport.h kleo/keyresolver.cpp kleo/keyresolver.h kleo/keyresolvercore.cpp kleo/keyresolvercore.h kleo/keyserverconfig.cpp kleo/keyserverconfig.h kleo/kleoexception.cpp kleo/kleoexception.h kleo/oidmap.cpp kleo/oidmap.h kleo/predicates.h kleo/stl_util.h models/keycache.cpp models/keycache.h models/keycache_p.h models/keylist.h models/keylistmodel.cpp models/keylistmodel.h models/keylistmodelinterface.cpp models/keylistmodelinterface.h models/keylistsortfilterproxymodel.cpp models/keylistsortfilterproxymodel.h models/keyrearrangecolumnsproxymodel.cpp models/keyrearrangecolumnsproxymodel.h models/subkeylistmodel.cpp models/subkeylistmodel.h models/useridlistmodel.cpp models/useridlistmodel.h + models/useridproxymodel.cpp + models/useridproxymodel.h utils/algorithm.h utils/assuan.cpp utils/assuan.h utils/chrono.h utils/classify.cpp utils/classify.h utils/compat.cpp utils/compat.h utils/compliance.cpp utils/compliance.h utils/cryptoconfig.cpp utils/cryptoconfig.h utils/cryptoconfig_p.h utils/filesystemwatcher.cpp utils/filesystemwatcher.h utils/formatting.cpp utils/formatting.h utils/gnupg-registry.c utils/gnupg-registry.h utils/gnupg.cpp utils/gnupg.h utils/hex.cpp utils/hex.h utils/keyhelpers.cpp utils/keyhelpers.h utils/keyusage.h utils/qtstlhelpers.cpp utils/qtstlhelpers.h utils/scdaemon.cpp utils/scdaemon.h utils/stringutils.cpp utils/stringutils.h utils/systeminfo.cpp utils/systeminfo.h utils/test.cpp utils/test.h utils/uniquelock.cpp utils/uniquelock.h ) ecm_qt_declare_logging_category(KPim6Libkleo HEADER libkleo_debug.h IDENTIFIER LIBKLEO_LOG CATEGORY_NAME org.kde.pim.libkleo DESCRIPTION "libkleo (kleo_core)" EXPORT LIBKLEO ) target_sources(KPim6Libkleo PRIVATE + ui/adjustingscrollarea.cpp + ui/adjustingscrollarea.h ui/auditlogviewer.cpp ui/auditlogviewer.h ui/cryptoconfigentryreaderport.cpp ui/cryptoconfigentryreaderport_p.h ui/cryptoconfigmodule.cpp ui/cryptoconfigmodule.h ui/cryptoconfigmodule_p.h ui/directoryserviceswidget.cpp ui/directoryserviceswidget.h ui/dnattributeorderconfigwidget.cpp ui/dnattributeorderconfigwidget.h ui/editdirectoryservicedialog.cpp ui/editdirectoryservicedialog.h ui/filenamerequester.cpp ui/filenamerequester.h ui/messagebox.cpp ui/messagebox.h ui/navigatabletreeview.cpp ui/navigatabletreeview.h ui/navigatabletreewidget.cpp ui/navigatabletreewidget.h ui/progressbar.cpp ui/progressbar.h ui/progressdialog.cpp ui/progressdialog.h ui/readerportselection.cpp ui/readerportselection.h ) ecm_qt_declare_logging_category(KPim6Libkleo HEADER kleo_ui_debug.h IDENTIFIER KLEO_UI_LOG CATEGORY_NAME org.kde.pim.kleo_ui DESCRIPTION "libkleo (kleo_ui)" OLD_CATEGORY_NAMES log_kleo_ui EXPORT LIBKLEO ) target_sources(KPim6Libkleo PRIVATE # make this a separate lib. ui/keyapprovaldialog.cpp ui/keyapprovaldialog.h ui/keylistview.cpp ui/keylistview.h ui/keyrequester.cpp ui/keyrequester.h ui/keyselectioncombo.cpp ui/keyselectioncombo.h ui/keyselectiondialog.cpp ui/keyselectiondialog.h ui/newkeyapprovaldialog.cpp ui/newkeyapprovaldialog.h ) if(MINGW) # we do not care about different signedness of passed pointer arguments set_source_files_properties(utils/gnupg-registry.c PROPERTIES COMPILE_OPTIONS "-Wno-pointer-sign") endif() target_link_libraries(KPim6Libkleo PUBLIC Gpgmepp PRIVATE Qt::Widgets KF6::I18n KF6::Completion KF6::ConfigCore KF6::ColorScheme KF6::ConfigGui KF6::CoreAddons KF6::WidgetsAddons KF6::ItemModels KF6::Codecs LibGpgError::LibGpgError) target_link_libraries(KPim6Libkleo PUBLIC QGpgmeQt6) # Boost::headers may not be available for old versions of Boost if (TARGET Boost::headers) target_link_libraries(KPim6Libkleo PRIVATE Boost::headers) endif() if (TARGET KF6::TextCustomEditor) add_definitions(-DHAVE_PIMTEXTEDIT) target_link_libraries(KPim6Libkleo PRIVATE KF6::TextCustomEditor) endif() if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) set_target_properties(KPim6Libkleo PROPERTIES UNITY_BUILD ON) endif() ecm_generate_export_header(KPim6Libkleo BASE_NAME kleo VERSION ${PIM_VERSION} DEPRECATED_BASE_VERSION 0 DEPRECATION_VERSIONS 5.23 ) if(WIN32) target_link_libraries(KPim6Libkleo ${GPGME_VANILLA_LIBRARIES} ) endif() set_target_properties(KPim6Libkleo PROPERTIES VERSION ${LIBKLEO_VERSION} SOVERSION ${LIBKLEO_SOVERSION} EXPORT_NAME Libkleo ) install(TARGETS KPim6Libkleo EXPORT KPim6LibkleoTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) target_include_directories(KPim6Libkleo INTERFACE "$") target_include_directories(KPim6Libkleo PUBLIC "$") ecm_generate_headers(libkleo_CamelCase_HEADERS HEADER_NAMES AuditLogEntry ChecksumDefinition Debug DefaultKeyFilter DefaultKeyGenerationJob DocAction Dn Enum ExpiryChecker ExpiryCheckerConfig ExpiryCheckerSettings KConfigBasedKeyFilter KeyFilter KeyFilterManager KeyGroup KeyGroupConfig KeyGroupImportExport KeyResolver KeyResolverCore KeyserverConfig KleoException OidMap Predicates Stl_Util REQUIRED_HEADERS libkleo_HEADERS PREFIX Libkleo RELATIVE kleo ) ecm_generate_headers(libkleo_CamelCase_models_HEADERS HEADER_NAMES KeyCache KeyList KeyListModel KeyListModelInterface KeyListSortFilterProxyModel KeyRearrangeColumnsProxyModel SubkeyListModel UserIDListModel + UserIDProxyModel REQUIRED_HEADERS libkleo_models_HEADERS PREFIX Libkleo RELATIVE models ) ecm_generate_headers(libkleo_CamelCase_utils_HEADERS HEADER_NAMES Algorithm Assuan Chrono Classify Compat Compliance CryptoConfig FileSystemWatcher Formatting GnuPG Hex KeyHelpers KeyUsage QtStlHelpers SCDaemon StringUtils SystemInfo Test UniqueLock REQUIRED_HEADERS libkleo_utils_HEADERS PREFIX Libkleo RELATIVE utils ) ecm_generate_headers(libkleo_CamelCase_ui_HEADERS HEADER_NAMES + AdjustingScrollArea AuditLogViewer CryptoConfigModule DNAttributeOrderConfigWidget DirectoryServicesWidget EditDirectoryServiceDialog FileNameRequester KeyApprovalDialog KeyListView KeyRequester KeySelectionCombo KeySelectionDialog MessageBox NavigatableTreeView NavigatableTreeWidget NewKeyApprovalDialog ProgressDialog ReaderPortSelection REQUIRED_HEADERS libkleo_ui_HEADERS PREFIX Libkleo RELATIVE ui ) kconfig_add_kcfg_files(KPim6Libkleo kcfg/expirycheckerconfigbase.kcfgc kcfg/classifyconfig.kcfgc ) install(FILES ${libkleo_CamelCase_HEADERS} ${libkleo_CamelCase_models_HEADERS} ${libkleo_CamelCase_ui_HEADERS} ${libkleo_CamelCase_utils_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KPim6/Libkleo/Libkleo COMPONENT Devel ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/expirycheckerconfigbase.h ${CMAKE_CURRENT_BINARY_DIR}/kleo_export.h ${libkleo_HEADERS} ${libkleo_models_HEADERS} ${libkleo_ui_HEADERS} ${libkleo_utils_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/classifyconfig.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KPim6/Libkleo/libkleo COMPONENT Devel ) if ( WIN32 ) install ( FILES libkleopatrarc-win32.desktop DESTINATION ${KDE_INSTALL_CONFDIR} RENAME libkleopatrarc ) else () install ( FILES libkleopatrarc.desktop DESTINATION ${KDE_INSTALL_CONFDIR} RENAME libkleopatrarc ) endif () if (BUILD_QCH) ecm_add_qch( KPim6Libkleo_QCH NAME KPim6Libkleo BASE_NAME KPim6Libkleo VERSION ${PIM_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${libkleo_HEADERS} ${libkleo_models_HEADERS} ${libkleo_ui_HEADERS} ${libkleo_utils_HEADERS} #MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" #IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics" LINK_QCHS Qt6Core_QCH Qt6Gui_QCH Qt6Widgets_QCH INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR} BLANK_MACROS KLEO_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() diff --git a/src/kleo/defaultkeyfilter.cpp b/src/kleo/defaultkeyfilter.cpp index ee6c9c284..9fe76bf93 100644 --- a/src/kleo/defaultkeyfilter.cpp +++ b/src/kleo/defaultkeyfilter.cpp @@ -1,589 +1,726 @@ /* defaultkeyfilter.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "defaultkeyfilter.h" +#include "utils/compliance.h" #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE #else #include #endif #include #include #include #include using namespace GpgME; using namespace Kleo; static bool is_card_key(const Key &key) { const std::vector sks = key.subkeys(); return std::find_if(sks.begin(), sks.end(), std::mem_fn(&Subkey::isCardKey)) != sks.end(); } class DefaultKeyFilter::Private { public: Private() { } QColor mFgColor; QColor mBgColor; QString mName; QString mIcon; QString mId; MatchContexts mMatchContexts = AnyMatchContext; unsigned int mSpecificity = 0; bool mItalic = false; bool mBold = false; bool mStrikeOut = false; bool mUseFullFont = false; QFont mFont; TriState mRevoked = DoesNotMatter; TriState mExpired = DoesNotMatter; TriState mInvalid = DoesNotMatter; TriState mDisabled = DoesNotMatter; TriState mRoot = DoesNotMatter; TriState mCanEncrypt = DoesNotMatter; TriState mCanSign = DoesNotMatter; TriState mCanCertify = DoesNotMatter; TriState mCanAuthenticate = DoesNotMatter; TriState mHasEncrypt = DoesNotMatter; TriState mHasSign = DoesNotMatter; TriState mHasCertify = DoesNotMatter; TriState mHasAuthenticate = DoesNotMatter; TriState mQualified = DoesNotMatter; TriState mCardKey = DoesNotMatter; TriState mHasSecret = DoesNotMatter; TriState mIsOpenPGP = DoesNotMatter; TriState mWasValidated = DoesNotMatter; TriState mIsDeVs = DoesNotMatter; TriState mBad = DoesNotMatter; TriState mValidIfSMIME = DoesNotMatter; LevelState mOwnerTrust = LevelDoesNotMatter; GpgME::Key::OwnerTrust mOwnerTrustReferenceLevel = Key::OwnerTrust::Unknown; LevelState mValidity = LevelDoesNotMatter; GpgME::UserID::Validity mValidityReferenceLevel = UserID::Validity::Unknown; }; DefaultKeyFilter::DefaultKeyFilter() : KeyFilter{} , d{new Private} { } DefaultKeyFilter::~DefaultKeyFilter() = default; bool DefaultKeyFilter::matches(const Key &key, MatchContexts contexts) const { if (!(d->mMatchContexts & contexts)) { return false; } #ifdef MATCH #undef MATCH #endif #define MATCH(member, method) \ do { \ if (member != DoesNotMatter && key.method() != bool(member == Set)) { \ return false; \ } \ } while (false) #define IS_MATCH(what) MATCH(d->m##what, is##what) #define CAN_MATCH(what) MATCH(d->mCan##what, can##what) #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE #define HAS_MATCH(what) MATCH(d->mHas##what, has##what) #else #define HAS_MATCH(what) \ do { \ if (d->mHas##what != DoesNotMatter && Kleo::keyHas##what(key) != bool(d->mHas##what == Set)) { \ return false; \ } \ } while (false) #endif IS_MATCH(Revoked); IS_MATCH(Expired); IS_MATCH(Invalid); IS_MATCH(Disabled); IS_MATCH(Root); CAN_MATCH(Encrypt); CAN_MATCH(Sign); CAN_MATCH(Certify); CAN_MATCH(Authenticate); HAS_MATCH(Encrypt); HAS_MATCH(Sign); HAS_MATCH(Certify); HAS_MATCH(Authenticate); IS_MATCH(Qualified); if (d->mCardKey != DoesNotMatter) { if ((d->mCardKey == Set && !is_card_key(key)) || (d->mCardKey == NotSet && is_card_key(key))) { return false; } } MATCH(d->mHasSecret, hasSecret); #undef MATCH if (d->mIsOpenPGP != DoesNotMatter && bool(key.protocol() == GpgME::OpenPGP) != bool(d->mIsOpenPGP == Set)) { return false; } if (d->mWasValidated != DoesNotMatter && bool(key.keyListMode() & GpgME::Validate) != bool(d->mWasValidated == Set)) { return false; } if (d->mIsDeVs != DoesNotMatter && bool(DeVSCompliance::keyIsCompliant(key)) != bool(d->mIsDeVs == Set)) { return false; } if (d->mBad != DoesNotMatter && /* This is similar to GPGME::Key::isBad which was introduced in GPGME 1.13.0 */ bool(key.isNull() || key.isRevoked() || key.isExpired() || key.isDisabled() || key.isInvalid()) != bool(d->mBad == Set)) { return false; } const UserID uid = key.userID(0); if ((key.protocol() == GpgME::CMS) // && (d->mValidIfSMIME != DoesNotMatter) // && (bool(uid.validity() >= UserID::Full) != bool(d->mValidIfSMIME == Set))) { return false; } switch (d->mOwnerTrust) { default: case LevelDoesNotMatter: break; case Is: if (key.ownerTrust() != d->mOwnerTrustReferenceLevel) { return false; } break; case IsNot: if (key.ownerTrust() == d->mOwnerTrustReferenceLevel) { return false; } break; case IsAtLeast: if (static_cast(key.ownerTrust()) < static_cast(d->mOwnerTrustReferenceLevel)) { return false; } break; case IsAtMost: if (static_cast(key.ownerTrust()) > static_cast(d->mOwnerTrustReferenceLevel)) { return false; } break; } switch (d->mValidity) { default: case LevelDoesNotMatter: break; case Is: if (uid.validity() != d->mValidityReferenceLevel) { return false; } break; case IsNot: if (uid.validity() == d->mValidityReferenceLevel) { return false; } break; case IsAtLeast: if (static_cast(uid.validity()) < static_cast(d->mValidityReferenceLevel)) { return false; } break; case IsAtMost: if (static_cast(uid.validity()) > static_cast(d->mValidityReferenceLevel)) { return false; } break; } return true; } +bool DefaultKeyFilter::matches(const UserID &userID, MatchContexts contexts) const +{ + if (!(d->mMatchContexts & contexts)) { + return false; + } +#ifdef MATCH_KEY +#undef MATCH_KEY +#endif +#define MATCH_KEY(member, method) \ + do { \ + if (member != DoesNotMatter && userID.parent().method() != bool(member == Set)) { \ + return false; \ + } \ + } while (false) +#define IS_MATCH_KEY(what) MATCH_KEY(d->m##what, is##what) +#define CAN_MATCH_KEY(what) MATCH_KEY(d->mCan##what, can##what) +#if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE +#define HAS_MATCH_KEY(what) MATCH_KEY(d->mHas##what, has##what) +#else +#define HAS_MATCH_KEY(what) \ + do { \ + if (d->mHas##what != DoesNotMatter && Kleo::keyHas##what(userID.parent()) != bool(d->mHas##what == Set)) { \ + return false; \ + } \ + } while (false) +#endif + +#ifdef MATCH +#undef MATCH +#endif +#define MATCH(member, method) \ + do { \ + if (member != DoesNotMatter && (userID.parent().method() != bool(member == Set) || userID.method() != bool(member == Set))) { \ + return false; \ + } \ + } while (false) +#define IS_MATCH(what) MATCH(d->m##what, is##what) + IS_MATCH(Revoked); + IS_MATCH_KEY(Expired); + // We have to do this manually since there's no UserID::isExpired() + if (d->mExpired != DoesNotMatter && (userID.parent().isExpired() != bool(d->mExpired == Set) || isExpired(userID) != bool(d->mExpired == Set))) { + return false; + } + IS_MATCH(Invalid); + IS_MATCH_KEY(Disabled); + IS_MATCH_KEY(Root); + CAN_MATCH_KEY(Encrypt); + CAN_MATCH_KEY(Sign); + CAN_MATCH_KEY(Certify); + CAN_MATCH_KEY(Authenticate); + HAS_MATCH_KEY(Encrypt); + HAS_MATCH_KEY(Sign); + HAS_MATCH_KEY(Certify); + HAS_MATCH_KEY(Authenticate); + IS_MATCH_KEY(Qualified); + if (d->mCardKey != DoesNotMatter) { + if ((d->mCardKey == Set && !is_card_key(userID.parent())) || (d->mCardKey == NotSet && is_card_key(userID.parent()))) { + return false; + } + } + MATCH_KEY(d->mHasSecret, hasSecret); +#undef MATCH + if (d->mIsOpenPGP != DoesNotMatter && bool(userID.parent().protocol() == GpgME::OpenPGP) != bool(d->mIsOpenPGP == Set)) { + return false; + } + if (d->mWasValidated != DoesNotMatter && bool(userID.parent().keyListMode() & GpgME::Validate) != bool(d->mWasValidated == Set)) { + return false; + } + if (d->mIsDeVs != DoesNotMatter && bool(DeVSCompliance::userIDIsCompliant(userID)) != bool(d->mIsDeVs == Set)) { + return false; + } + if (d->mBad != DoesNotMatter && + /* This is similar to GPGME::Key::isBad which was introduced in GPGME 1.13.0 */ + bool(userID.parent().isNull() || userID.isNull() || userID.parent().isRevoked() || userID.isRevoked() || userID.parent().isExpired() + || userID.parent().isDisabled() || userID.parent().isInvalid() || userID.isInvalid()) + != bool(d->mBad == Set)) { + return false; + } + if ((userID.parent().protocol() == GpgME::CMS) // + && (d->mValidIfSMIME != DoesNotMatter) // + && (bool(userID.validity() >= UserID::Full) != bool(d->mValidIfSMIME == Set))) { + return false; + } + switch (d->mOwnerTrust) { + default: + case LevelDoesNotMatter: + break; + case Is: + if (userID.parent().ownerTrust() != d->mOwnerTrustReferenceLevel) { + return false; + } + break; + case IsNot: + if (userID.parent().ownerTrust() == d->mOwnerTrustReferenceLevel) { + return false; + } + break; + case IsAtLeast: + if (static_cast(userID.parent().ownerTrust()) < static_cast(d->mOwnerTrustReferenceLevel)) { + return false; + } + break; + case IsAtMost: + if (static_cast(userID.parent().ownerTrust()) > static_cast(d->mOwnerTrustReferenceLevel)) { + return false; + } + break; + } + switch (d->mValidity) { + default: + case LevelDoesNotMatter: + break; + case Is: + if (userID.validity() != d->mValidityReferenceLevel) { + return false; + } + break; + case IsNot: + if (userID.validity() == d->mValidityReferenceLevel) { + return false; + } + break; + case IsAtLeast: + if (static_cast(userID.validity()) < static_cast(d->mValidityReferenceLevel)) { + return false; + } + break; + case IsAtMost: + if (static_cast(userID.validity()) > static_cast(d->mValidityReferenceLevel)) { + return false; + } + break; + } + return true; +} + KeyFilter::FontDescription DefaultKeyFilter::fontDescription() const { if (d->mUseFullFont) { return FontDescription::create(font(), bold(), italic(), strikeOut()); } else { return FontDescription::create(bold(), italic(), strikeOut()); } } void DefaultKeyFilter::setFgColor(const QColor &value) { d->mFgColor = value; } void DefaultKeyFilter::setBgColor(const QColor &value) { d->mBgColor = value; } void DefaultKeyFilter::setName(const QString &value) { d->mName = value; } void DefaultKeyFilter::setIcon(const QString &value) { d->mIcon = value; } void DefaultKeyFilter::setId(const QString &value) { d->mId = value; } void DefaultKeyFilter::setMatchContexts(MatchContexts value) { d->mMatchContexts = value; } void DefaultKeyFilter::setSpecificity(unsigned int value) { d->mSpecificity = value; } void DefaultKeyFilter::setItalic(bool value) { d->mItalic = value; } void DefaultKeyFilter::setBold(bool value) { d->mBold = value; } void DefaultKeyFilter::setStrikeOut(bool value) { d->mStrikeOut = value; } void DefaultKeyFilter::setUseFullFont(bool value) { d->mUseFullFont = value; } void DefaultKeyFilter::setFont(const QFont &value) { d->mFont = value; } void DefaultKeyFilter::setRevoked(DefaultKeyFilter::TriState value) { d->mRevoked = value; } void DefaultKeyFilter::setExpired(DefaultKeyFilter::TriState value) { d->mExpired = value; } void DefaultKeyFilter::setInvalid(DefaultKeyFilter::TriState value) { d->mInvalid = value; } void DefaultKeyFilter::setDisabled(DefaultKeyFilter::TriState value) { d->mDisabled = value; } void DefaultKeyFilter::setRoot(DefaultKeyFilter::TriState value) { d->mRoot = value; } void DefaultKeyFilter::setCanEncrypt(DefaultKeyFilter::TriState value) { d->mCanEncrypt = value; } void DefaultKeyFilter::setCanSign(DefaultKeyFilter::TriState value) { d->mCanSign = value; } void DefaultKeyFilter::setCanCertify(DefaultKeyFilter::TriState value) { d->mCanCertify = value; } void DefaultKeyFilter::setCanAuthenticate(DefaultKeyFilter::TriState value) { d->mCanAuthenticate = value; } void DefaultKeyFilter::setHasEncrypt(DefaultKeyFilter::TriState value) { d->mHasEncrypt = value; } void DefaultKeyFilter::setHasSign(DefaultKeyFilter::TriState value) { d->mHasSign = value; } void DefaultKeyFilter::setHasCertify(DefaultKeyFilter::TriState value) { d->mHasCertify = value; } void DefaultKeyFilter::setHasAuthenticate(DefaultKeyFilter::TriState value) { d->mHasAuthenticate = value; } void DefaultKeyFilter::setQualified(DefaultKeyFilter::TriState value) { d->mQualified = value; } void DefaultKeyFilter::setCardKey(DefaultKeyFilter::TriState value) { d->mCardKey = value; } void DefaultKeyFilter::setHasSecret(DefaultKeyFilter::TriState value) { d->mHasSecret = value; } void DefaultKeyFilter::setIsOpenPGP(DefaultKeyFilter::TriState value) { d->mIsOpenPGP = value; } void DefaultKeyFilter::setWasValidated(DefaultKeyFilter::TriState value) { d->mWasValidated = value; } void DefaultKeyFilter::setOwnerTrust(DefaultKeyFilter::LevelState value) { d->mOwnerTrust = value; } void DefaultKeyFilter::setOwnerTrustReferenceLevel(GpgME::Key::OwnerTrust value) { d->mOwnerTrustReferenceLevel = value; } void DefaultKeyFilter::setValidity(DefaultKeyFilter::LevelState value) { d->mValidity = value; } void DefaultKeyFilter::setValidityReferenceLevel(GpgME::UserID::Validity value) { d->mValidityReferenceLevel = value; } void DefaultKeyFilter::setIsDeVs(DefaultKeyFilter::TriState value) { d->mIsDeVs = value; } void DefaultKeyFilter::setIsBad(DefaultKeyFilter::TriState value) { d->mBad = value; } void DefaultKeyFilter::setValidIfSMIME(DefaultKeyFilter::TriState value) { d->mValidIfSMIME = value; } QColor DefaultKeyFilter::fgColor() const { return d->mFgColor; } QColor DefaultKeyFilter::bgColor() const { return d->mBgColor; } QString DefaultKeyFilter::name() const { return d->mName; } QString DefaultKeyFilter::icon() const { return d->mIcon; } QString DefaultKeyFilter::id() const { return d->mId; } QFont DefaultKeyFilter::font() const { return d->mFont; } KeyFilter::MatchContexts DefaultKeyFilter::availableMatchContexts() const { return d->mMatchContexts; } unsigned int DefaultKeyFilter::specificity() const { return d->mSpecificity; } bool DefaultKeyFilter::italic() const { return d->mItalic; } bool DefaultKeyFilter::bold() const { return d->mBold; } bool DefaultKeyFilter::strikeOut() const { return d->mStrikeOut; } bool DefaultKeyFilter::useFullFont() const { return d->mUseFullFont; } DefaultKeyFilter::TriState DefaultKeyFilter::revoked() const { return d->mRevoked; } DefaultKeyFilter::TriState DefaultKeyFilter::expired() const { return d->mExpired; } DefaultKeyFilter::TriState DefaultKeyFilter::invalid() const { return d->mInvalid; } DefaultKeyFilter::TriState DefaultKeyFilter::disabled() const { return d->mDisabled; } DefaultKeyFilter::TriState DefaultKeyFilter::root() const { return d->mRoot; } DefaultKeyFilter::TriState DefaultKeyFilter::canEncrypt() const { return d->mCanEncrypt; } DefaultKeyFilter::TriState DefaultKeyFilter::canSign() const { return d->mCanSign; } DefaultKeyFilter::TriState DefaultKeyFilter::canCertify() const { return d->mCanCertify; } DefaultKeyFilter::TriState DefaultKeyFilter::canAuthenticate() const { return d->mCanAuthenticate; } DefaultKeyFilter::TriState DefaultKeyFilter::hasEncrypt() const { return d->mHasEncrypt; } DefaultKeyFilter::TriState DefaultKeyFilter::hasSign() const { return d->mHasSign; } DefaultKeyFilter::TriState DefaultKeyFilter::hasCertify() const { return d->mHasCertify; } DefaultKeyFilter::TriState DefaultKeyFilter::hasAuthenticate() const { return d->mHasAuthenticate; } DefaultKeyFilter::TriState DefaultKeyFilter::qualified() const { return d->mQualified; } DefaultKeyFilter::TriState DefaultKeyFilter::cardKey() const { return d->mCardKey; } DefaultKeyFilter::TriState DefaultKeyFilter::hasSecret() const { return d->mHasSecret; } DefaultKeyFilter::TriState DefaultKeyFilter::isOpenPGP() const { return d->mIsOpenPGP; } DefaultKeyFilter::TriState DefaultKeyFilter::wasValidated() const { return d->mWasValidated; } DefaultKeyFilter::LevelState DefaultKeyFilter::ownerTrust() const { return d->mOwnerTrust; } GpgME::Key::OwnerTrust DefaultKeyFilter::ownerTrustReferenceLevel() const { return d->mOwnerTrustReferenceLevel; } DefaultKeyFilter::LevelState DefaultKeyFilter::validity() const { return d->mValidity; } GpgME::UserID::Validity DefaultKeyFilter::validityReferenceLevel() const { return d->mValidityReferenceLevel; } DefaultKeyFilter::TriState DefaultKeyFilter::isDeVS() const { return d->mIsDeVs; } DefaultKeyFilter::TriState DefaultKeyFilter::isBad() const { return d->mBad; } DefaultKeyFilter::TriState DefaultKeyFilter::validIfSMIME() const { return d->mValidIfSMIME; } diff --git a/src/kleo/defaultkeyfilter.h b/src/kleo/defaultkeyfilter.h index 0cd0935de..f0ee12963 100644 --- a/src/kleo/defaultkeyfilter.h +++ b/src/kleo/defaultkeyfilter.h @@ -1,153 +1,154 @@ /* defaultkeyfilter.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "keyfilter.h" #include "kleo_export.h" #include #include #include #include #include namespace Kleo { /** Default implementation of key filter class. */ class KLEO_EXPORT DefaultKeyFilter : public KeyFilter { public: DefaultKeyFilter(); ~DefaultKeyFilter() override; /** Used for bool checks */ enum TriState { // clang-format off DoesNotMatter = 0, Set = 1, NotSet = 2, // clang-format on }; /** Used for level checks */ enum LevelState { // clang-format off LevelDoesNotMatter = 0, Is = 1, IsNot = 2, IsAtLeast = 3, IsAtMost = 4, // clang-format on }; bool matches(const GpgME::Key &key, MatchContexts ctx) const override; + bool matches(const GpgME::UserID &userID, MatchContexts ctx) const override; unsigned int specificity() const override; void setSpecificity(unsigned int value); QString id() const override; void setId(const QString &value); KeyFilter::MatchContexts availableMatchContexts() const override; void setMatchContexts(KeyFilter::MatchContexts value); QColor fgColor() const override; void setFgColor(const QColor &value); QColor bgColor() const override; void setBgColor(const QColor &value); FontDescription fontDescription() const override; QString name() const override; void setName(const QString &value); QString icon() const override; void setIcon(const QString &value); QFont font() const; void setFont(const QFont &value); TriState revoked() const; TriState expired() const; TriState invalid() const; TriState disabled() const; TriState root() const; TriState canEncrypt() const; TriState canSign() const; TriState canCertify() const; TriState canAuthenticate() const; TriState hasEncrypt() const; TriState hasSign() const; TriState hasCertify() const; TriState hasAuthenticate() const; TriState qualified() const; TriState cardKey() const; TriState hasSecret() const; TriState isOpenPGP() const; TriState wasValidated() const; TriState isDeVS() const; TriState isBad() const; LevelState ownerTrust() const; GpgME::Key::OwnerTrust ownerTrustReferenceLevel() const; LevelState validity() const; GpgME::UserID::Validity validityReferenceLevel() const; bool italic() const; bool bold() const; bool strikeOut() const; bool useFullFont() const; void setRevoked(const TriState); void setExpired(const TriState); void setInvalid(const TriState); void setDisabled(const TriState); void setRoot(const TriState); void setCanEncrypt(const TriState); void setCanSign(const TriState); void setCanCertify(const TriState); void setCanAuthenticate(const TriState); void setHasEncrypt(const TriState); void setHasSign(const TriState); void setHasCertify(const TriState); void setHasAuthenticate(const TriState); void setQualified(const TriState); void setCardKey(const TriState); void setHasSecret(const TriState); void setIsOpenPGP(const TriState); void setWasValidated(const TriState); void setIsDeVs(const TriState); void setIsBad(const TriState); /** * If \p value is \c Set, then invalid S/MIME certificates do not match. * If \p value is \c NotSet, then valid S/MIME certificates do not match. */ void setValidIfSMIME(TriState value); TriState validIfSMIME() const; void setOwnerTrust(const LevelState); void setOwnerTrustReferenceLevel(const GpgME::Key::OwnerTrust); void setValidity(const LevelState); void setValidityReferenceLevel(const GpgME::UserID::Validity); void setItalic(bool value); void setBold(bool value); void setStrikeOut(bool value); void setUseFullFont(bool value); private: class Private; const std::unique_ptr d; }; } // namespace Kleo diff --git a/src/kleo/keyfilter.h b/src/kleo/keyfilter.h index 6c5be6454..ac14e1cc9 100644 --- a/src/kleo/keyfilter.h +++ b/src/kleo/keyfilter.h @@ -1,105 +1,107 @@ /* keyfilter.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include #include namespace GpgME { class Key; +class UserID; } class QFont; class QColor; class QString; namespace Kleo { /** @short An abstract base class key filters */ class KLEO_EXPORT KeyFilter { public: virtual ~KeyFilter() { } enum MatchContext { // clang-format off NoMatchContext = 0x0, Appearance = 0x1, Filtering = 0x2, AnyMatchContext = Appearance | Filtering // clang-format on }; Q_DECLARE_FLAGS(MatchContexts, MatchContext) virtual bool matches(const GpgME::Key &key, MatchContexts ctx) const = 0; + virtual bool matches(const GpgME::UserID &userID, MatchContexts ctx) const = 0; virtual unsigned int specificity() const = 0; virtual QString id() const = 0; virtual MatchContexts availableMatchContexts() const = 0; // not sure if we want these here, but for the time being, it's // the easiest way: virtual QColor fgColor() const = 0; virtual QColor bgColor() const = 0; virtual QString name() const = 0; virtual QString icon() const = 0; class FontDescription { public: FontDescription(); FontDescription(const FontDescription &other); FontDescription &operator=(const FontDescription &other) { FontDescription copy(other); swap(copy); return *this; } ~FontDescription(); static FontDescription create(bool bold, bool italic, bool strikeOut); static FontDescription create(const QFont &font, bool bold, bool italic, bool strikeOut); QFont font(const QFont &base) const; FontDescription resolve(const FontDescription &other) const; void swap(FontDescription &other) { std::swap(this->d, other.d); } struct Private; private: std::unique_ptr d; }; virtual FontDescription fontDescription() const = 0; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KeyFilter::MatchContexts) } #include Q_DECLARE_METATYPE(Kleo::KeyFilter::MatchContexts) diff --git a/src/kleo/keyfiltermanager.cpp b/src/kleo/keyfiltermanager.cpp index a364a9d10..1a0513405 100644 --- a/src/kleo/keyfiltermanager.cpp +++ b/src/kleo/keyfiltermanager.cpp @@ -1,461 +1,483 @@ /* keyfiltermanager.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keyfiltermanager.h" #include "defaultkeyfilter.h" #include "kconfigbasedkeyfilter.h" #include "stl_util.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace GpgME; namespace { class Model : public QAbstractListModel { KeyFilterManager::Private *m_keyFilterManagerPrivate; public: explicit Model(KeyFilterManager::Private *p) : QAbstractListModel(nullptr) , m_keyFilterManagerPrivate(p) { } int rowCount(const QModelIndex &) const override; QVariant data(const QModelIndex &idx, int role) const override; /* upgrade to public */ using QAbstractListModel::beginResetModel; /* upgrade to public */ using QAbstractListModel::endResetModel; }; class AllCertificatesKeyFilter : public DefaultKeyFilter { public: AllCertificatesKeyFilter() : DefaultKeyFilter() { setSpecificity(UINT_MAX); // overly high for ordering setName(i18n("All Certificates")); setId(QStringLiteral("all-certificates")); setMatchContexts(Filtering); } }; class MyCertificatesKeyFilter : public DefaultKeyFilter { public: MyCertificatesKeyFilter() : DefaultKeyFilter() { setHasSecret(Set); setSpecificity(UINT_MAX - 1); // overly high for ordering setName(i18n("My Certificates")); setId(QStringLiteral("my-certificates")); setMatchContexts(AnyMatchContext); setBold(true); } }; class TrustedCertificatesKeyFilter : public DefaultKeyFilter { public: TrustedCertificatesKeyFilter() : DefaultKeyFilter() { setRevoked(NotSet); setValidity(IsAtLeast); setValidityReferenceLevel(UserID::Marginal); setSpecificity(UINT_MAX - 2); // overly high for ordering setName(i18n("Trusted Certificates")); setId(QStringLiteral("trusted-certificates")); setMatchContexts(Filtering); } }; class FullCertificatesKeyFilter : public DefaultKeyFilter { public: FullCertificatesKeyFilter() : DefaultKeyFilter() { setRevoked(NotSet); setValidity(IsAtLeast); setValidityReferenceLevel(UserID::Full); setSpecificity(UINT_MAX - 3); setName(i18n("Fully Trusted Certificates")); setId(QStringLiteral("full-certificates")); setMatchContexts(Filtering); } }; class OtherCertificatesKeyFilter : public DefaultKeyFilter { public: OtherCertificatesKeyFilter() : DefaultKeyFilter() { setHasSecret(NotSet); setValidity(IsAtMost); setValidityReferenceLevel(UserID::Never); setSpecificity(UINT_MAX - 4); // overly high for ordering setName(i18n("Other Certificates")); setId(QStringLiteral("other-certificates")); setMatchContexts(Filtering); } }; /* This filter selects uncertified OpenPGP keys, i.e. "good" OpenPGP keys with * unrevoked user IDs that are not fully valid. */ class UncertifiedOpenPGPKeysFilter : public DefaultKeyFilter { public: UncertifiedOpenPGPKeysFilter() : DefaultKeyFilter() { setSpecificity(UINT_MAX - 6); // overly high for ordering setName(i18n("Not Certified Certificates")); setId(QStringLiteral("not-certified-certificates")); setMatchContexts(Filtering); setIsOpenPGP(Set); setIsBad(NotSet); } bool matches(const Key &key, MatchContexts contexts) const override { return DefaultKeyFilter::matches(key, contexts) && !Kleo::allUserIDsHaveFullValidity(key); } }; /* This filter selects only invalid keys (i.e. those where not all * UIDs are at least fully valid). */ class KeyNotValidFilter : public DefaultKeyFilter { public: KeyNotValidFilter() : DefaultKeyFilter() { setSpecificity(UINT_MAX - 7); // overly high for ordering setName(i18n("Not Validated Certificates")); setId(QStringLiteral("not-validated-certificates")); setMatchContexts(Filtering); } bool matches(const Key &key, MatchContexts contexts) const override { return DefaultKeyFilter::matches(key, contexts) && !Kleo::allUserIDsHaveFullValidity(key); } }; } static std::vector> defaultFilters() { std::vector> result; result.reserve(6); result.push_back(std::shared_ptr(new MyCertificatesKeyFilter)); result.push_back(std::shared_ptr(new TrustedCertificatesKeyFilter)); result.push_back(std::shared_ptr(new FullCertificatesKeyFilter)); result.push_back(std::shared_ptr(new OtherCertificatesKeyFilter)); result.push_back(std::shared_ptr(new AllCertificatesKeyFilter)); result.push_back(std::shared_ptr(new UncertifiedOpenPGPKeysFilter)); result.push_back(std::shared_ptr(new KeyNotValidFilter)); return result; } class KeyFilterManager::Private { public: Private() : filters() , model(this) { } void clear() { model.beginResetModel(); filters.clear(); model.endResetModel(); } std::vector> filters; Model model; GpgME::Protocol protocol = GpgME::UnknownProtocol; }; KeyFilterManager *KeyFilterManager::mSelf = nullptr; KeyFilterManager::KeyFilterManager(QObject *parent) : QObject(parent) , d(new Private) { mSelf = this; // ### DF: doesn't a KStaticDeleter work more reliably? if (QCoreApplication *app = QCoreApplication::instance()) { connect(app, &QCoreApplication::aboutToQuit, this, &QObject::deleteLater); } reload(); } KeyFilterManager::~KeyFilterManager() { mSelf = nullptr; if (d) { d->clear(); } } KeyFilterManager *KeyFilterManager::instance() { if (!mSelf) { mSelf = new KeyFilterManager(); } return mSelf; } void KeyFilterManager::alwaysFilterByProtocol(GpgME::Protocol protocol) { if (protocol != d->protocol) { d->protocol = protocol; reload(); } } const std::shared_ptr &KeyFilterManager::filterMatching(const Key &key, KeyFilter::MatchContexts contexts) const { const auto it = std::find_if(d->filters.cbegin(), d->filters.cend(), [&key, contexts](const std::shared_ptr &filter) { return filter->matches(key, contexts); }); if (it != d->filters.cend()) { return *it; } static const std::shared_ptr null; return null; } std::vector> KeyFilterManager::filtersMatching(const Key &key, KeyFilter::MatchContexts contexts) const { std::vector> result; result.reserve(d->filters.size()); std::remove_copy_if(d->filters.begin(), d->filters.end(), std::back_inserter(result), [&key, contexts](const std::shared_ptr &filter) { return !filter->matches(key, contexts); }); return result; } namespace { static const auto byDecreasingSpecificity = [](const std::shared_ptr &lhs, const std::shared_ptr &rhs) { return lhs->specificity() > rhs->specificity(); }; } void KeyFilterManager::reload() { d->clear(); d->filters = defaultFilters(); KSharedConfigPtr config = KSharedConfig::openConfig(QStringLiteral("libkleopatrarc")); const QStringList groups = config->groupList().filter(QRegularExpression(QStringLiteral("^Key Filter #\\d+$"))); const bool ignoreDeVs = !DeVSCompliance::isCompliant(); for (QStringList::const_iterator it = groups.begin(); it != groups.end(); ++it) { const KConfigGroup cfg(config, *it); if (cfg.hasKey("is-de-vs") && ignoreDeVs) { /* Don't show de-vs filters in other compliance modes */ continue; } d->filters.push_back(std::shared_ptr(new KConfigBasedKeyFilter(cfg))); } std::stable_sort(d->filters.begin(), d->filters.end(), byDecreasingSpecificity); if (d->protocol != GpgME::UnknownProtocol) { // remove filters with conflicting isOpenPGP rule const auto conflictingValue = (d->protocol == GpgME::OpenPGP) ? DefaultKeyFilter::NotSet : DefaultKeyFilter::Set; Kleo::erase_if(d->filters, [conflictingValue](const auto &f) { const auto filter = std::dynamic_pointer_cast(f); Q_ASSERT(filter); return filter->isOpenPGP() == conflictingValue; }); // add isOpenPGP rule to all filters const auto isOpenPGPValue = (d->protocol == GpgME::OpenPGP) ? DefaultKeyFilter::Set : DefaultKeyFilter::NotSet; std::for_each(std::begin(d->filters), std::end(d->filters), [isOpenPGPValue](auto &f) { const auto filter = std::dynamic_pointer_cast(f); Q_ASSERT(filter); return filter->setIsOpenPGP(isOpenPGPValue); }); } qCDebug(LIBKLEO_LOG) << "KeyFilterManager::" << __func__ << "final filter count is" << d->filters.size(); } QAbstractItemModel *KeyFilterManager::model() const { return &d->model; } const std::shared_ptr &KeyFilterManager::keyFilterByID(const QString &id) const { const auto it = std::find_if(d->filters.begin(), d->filters.end(), [id](const std::shared_ptr &filter) { return filter->id() == id; }); if (it != d->filters.end()) { return *it; } static const std::shared_ptr null; return null; } const std::shared_ptr &KeyFilterManager::fromModelIndex(const QModelIndex &idx) const { if (!idx.isValid() || idx.model() != &d->model || idx.row() < 0 || static_cast(idx.row()) >= d->filters.size()) { static const std::shared_ptr null; return null; } return d->filters[idx.row()]; } QModelIndex KeyFilterManager::toModelIndex(const std::shared_ptr &kf) const { if (!kf) { return {}; } const auto pair = std::equal_range(d->filters.cbegin(), d->filters.cend(), kf, byDecreasingSpecificity); const auto it = std::find(pair.first, pair.second, kf); if (it != pair.second) { return d->model.index(it - d->filters.begin()); } else { return QModelIndex(); } } int Model::rowCount(const QModelIndex &) const { return m_keyFilterManagerPrivate->filters.size(); } QVariant Model::data(const QModelIndex &idx, int role) const { if (!idx.isValid() || idx.model() != this || idx.row() < 0 || static_cast(idx.row()) > m_keyFilterManagerPrivate->filters.size()) { return QVariant(); } const auto filter = m_keyFilterManagerPrivate->filters[idx.row()]; switch (role) { case Qt::DecorationRole: return filter->icon(); case Qt::DisplayRole: case Qt::EditRole: case Qt::ToolTipRole: /* Most useless tooltip ever. */ return filter->name(); case KeyFilterManager::FilterIdRole: return filter->id(); case KeyFilterManager::FilterMatchContextsRole: return QVariant::fromValue(filter->availableMatchContexts()); default: return QVariant(); } } static KeyFilter::FontDescription get_fontdescription(const std::vector> &filters, const Key &key, const KeyFilter::FontDescription &initial) { return kdtools::accumulate_if( filters.begin(), filters.end(), [&key](const std::shared_ptr &filter) { return filter->matches(key, KeyFilter::Appearance); }, initial, [](const KeyFilter::FontDescription &lhs, const std::shared_ptr &rhs) { return lhs.resolve(rhs->fontDescription()); }); } QFont KeyFilterManager::font(const Key &key, const QFont &baseFont) const { const KeyFilter::FontDescription fd = get_fontdescription(d->filters, key, KeyFilter::FontDescription()); return fd.font(baseFont); } static QColor get_color(const std::vector> &filters, const Key &key, QColor (KeyFilter::*fun)() const) { const auto it = std::find_if(filters.cbegin(), filters.cend(), [&fun, &key](const std::shared_ptr &filter) { return filter->matches(key, KeyFilter::Appearance) && (filter.get()->*fun)().isValid(); }); if (it == filters.cend()) { return {}; } else { return (it->get()->*fun)(); } } +static QColor get_color(const std::vector> &filters, const UserID &userID, QColor (KeyFilter::*fun)() const) +{ + const auto it = std::find_if(filters.cbegin(), filters.cend(), [&fun, &userID](const std::shared_ptr &filter) { + return filter->matches(userID, KeyFilter::Appearance) && (filter.get()->*fun)().isValid(); + }); + if (it == filters.cend()) { + return {}; + } else { + return (it->get()->*fun)(); + } +} + static QString get_string(const std::vector> &filters, const Key &key, QString (KeyFilter::*fun)() const) { const auto it = std::find_if(filters.cbegin(), filters.cend(), [&fun, &key](const std::shared_ptr &filter) { return filter->matches(key, KeyFilter::Appearance) && !(filter.get()->*fun)().isEmpty(); }); if (it == filters.cend()) { return QString(); } else { return (*it)->icon(); } } QColor KeyFilterManager::bgColor(const Key &key) const { return get_color(d->filters, key, &KeyFilter::bgColor); } QColor KeyFilterManager::fgColor(const Key &key) const { return get_color(d->filters, key, &KeyFilter::fgColor); } +QColor KeyFilterManager::bgColor(const UserID &userID) const +{ + return get_color(d->filters, userID, &KeyFilter::bgColor); +} + +QColor KeyFilterManager::fgColor(const UserID &userID) const +{ + return get_color(d->filters, userID, &KeyFilter::fgColor); +} + QIcon KeyFilterManager::icon(const Key &key) const { const QString icon = get_string(d->filters, key, &KeyFilter::icon); return icon.isEmpty() ? QIcon() : QIcon::fromTheme(icon); } #include "moc_keyfiltermanager.cpp" diff --git a/src/kleo/keyfiltermanager.h b/src/kleo/keyfiltermanager.h index 533637c8b..9892cf509 100644 --- a/src/kleo/keyfiltermanager.h +++ b/src/kleo/keyfiltermanager.h @@ -1,81 +1,84 @@ /* keyfiltermanager.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include #include #include #include namespace GpgME { class Key; +class UserID; } class QAbstractItemModel; class QModelIndex; class QFont; class QColor; class QIcon; namespace Kleo { class KLEO_EXPORT KeyFilterManager : public QObject { Q_OBJECT public: enum ModelRoles { FilterIdRole = Qt::UserRole, FilterMatchContextsRole, }; protected: explicit KeyFilterManager(QObject *parent = nullptr); ~KeyFilterManager() override; public: static KeyFilterManager *instance(); /** * Adds the rule that keys must match @p protocol to all filters. */ void alwaysFilterByProtocol(GpgME::Protocol protocol); const std::shared_ptr &filterMatching(const GpgME::Key &key, KeyFilter::MatchContexts contexts) const; std::vector> filtersMatching(const GpgME::Key &key, KeyFilter::MatchContexts contexts) const; QAbstractItemModel *model() const; const std::shared_ptr &keyFilterByID(const QString &id) const; const std::shared_ptr &fromModelIndex(const QModelIndex &mi) const; QModelIndex toModelIndex(const std::shared_ptr &kf) const; void reload(); QFont font(const GpgME::Key &key, const QFont &baseFont) const; QColor bgColor(const GpgME::Key &key) const; + QColor bgColor(const GpgME::UserID &userID) const; QColor fgColor(const GpgME::Key &key) const; + QColor fgColor(const GpgME::UserID &userID) const; QIcon icon(const GpgME::Key &key) const; class Private; private: std::unique_ptr d; static KeyFilterManager *mSelf; }; } diff --git a/src/kleo/predicates.h b/src/kleo/predicates.h index d0de72948..6ea19f5e3 100644 --- a/src/kleo/predicates.h +++ b/src/kleo/predicates.h @@ -1,116 +1,116 @@ /* -*- mode: c++; c-basic-offset:4 -*- - models/predicates.h + kleo/predicates.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include #include #include #include #include namespace Kleo { namespace _detail { inline int mystrcmp(const char *s1, const char *s2) { using namespace std; return s1 ? s2 ? strcmp(s1, s2) : 1 : s2 ? -1 : 0; } #define make_comparator_str_impl(Name, expr, cmp) \ template class Op> \ struct Name { \ typedef bool result_type; \ \ bool operator()(const char *lhs, const char *rhs) const \ { \ return Op()(cmp, 0); \ } \ \ bool operator()(const std::string &lhs, const std::string &rhs) const \ { \ return operator()(lhs.c_str(), rhs.c_str()); \ } \ bool operator()(const char *lhs, const std::string &rhs) const \ { \ return operator()(lhs, rhs.c_str()); \ } \ bool operator()(const std::string &lhs, const char *rhs) const \ { \ return operator()(lhs.c_str(), rhs); \ } \ \ template \ bool operator()(const T &lhs, const T &rhs) const \ { \ return operator()((lhs expr), (rhs expr)); \ } \ template \ bool operator()(const T &lhs, const char *rhs) const \ { \ return operator()((lhs expr), rhs); \ } \ template \ bool operator()(const char *lhs, const T &rhs) const \ { \ return operator()(lhs, (rhs expr)); \ } \ template \ bool operator()(const T &lhs, const std::string &rhs) const \ { \ return operator()((lhs expr), rhs); \ } \ template \ bool operator()(const std::string &lhs, const T &rhs) const \ { \ return operator()(lhs, (rhs expr)); \ } \ } #define make_comparator_str_fast(Name, expr) make_comparator_str_impl(Name, expr, _detail::mystrcmp(lhs, rhs)) #define make_comparator_str(Name, expr) make_comparator_str_impl(Name, expr, qstricmp(lhs, rhs)) make_comparator_str_fast(ByFingerprint, .primaryFingerprint()); make_comparator_str_fast(ByKeyID, .keyID()); make_comparator_str_fast(ByShortKeyID, .shortKeyID()); make_comparator_str_fast(ByChainID, .chainID()); make_comparator_str_fast(ByKeyGrip, .keyGrip()); template void sort_by_fpr(T &t) { std::sort(t.begin(), t.end(), ByFingerprint()); } template void remove_duplicates_by_fpr(T &t) { t.erase(std::unique(t.begin(), t.end(), ByFingerprint()), t.end()); } template T union_by_fpr(const T &t1, const T &t2) { T result; result.reserve(t1.size() + t2.size()); std::set_union(t1.begin(), // t1.end(), t2.begin(), t2.end(), std::back_inserter(result), ByFingerprint()); return result; } } } diff --git a/src/models/keycache.cpp b/src/models/keycache.cpp index 10a5b2ea9..8c4b70713 100644 --- a/src/models/keycache.cpp +++ b/src/models/keycache.cpp @@ -1,1765 +1,1773 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keycache.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007, 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2020, 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keycache.h" #include "keycache_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::chrono_literals; using namespace Kleo; using namespace GpgME; using namespace KMime::Types; static const unsigned int hours2ms = 1000 * 60 * 60; // // // KeyCache // // namespace { make_comparator_str(ByEMail, .first.c_str()); } class Kleo::KeyCacheAutoRefreshSuspension { KeyCacheAutoRefreshSuspension() { qCDebug(LIBKLEO_LOG) << __func__; auto cache = KeyCache::mutableInstance(); cache->enableFileSystemWatcher(false); m_refreshInterval = cache->refreshInterval(); cache->setRefreshInterval(0); cache->cancelKeyListing(); m_cache = cache; } public: ~KeyCacheAutoRefreshSuspension() { qCDebug(LIBKLEO_LOG) << __func__; if (auto cache = m_cache.lock()) { cache->enableFileSystemWatcher(true); cache->setRefreshInterval(m_refreshInterval); } } static std::shared_ptr instance() { static std::weak_ptr self; if (auto s = self.lock()) { return s; } else { s = std::shared_ptr{new KeyCacheAutoRefreshSuspension{}}; self = s; return s; } } private: std::weak_ptr m_cache; int m_refreshInterval = 0; }; class KeyCache::Private { friend class ::Kleo::KeyCache; KeyCache *const q; public: explicit Private(KeyCache *qq) : q(qq) , m_refreshInterval(1) , m_initalized(false) , m_pgpOnly(true) , m_remarks_enabled(false) { connect(&m_autoKeyListingTimer, &QTimer::timeout, q, [this]() { q->startKeyListing(); }); updateAutoKeyListingTimer(); } ~Private() { if (m_refreshJob) { m_refreshJob->cancel(); } } template class Op> class Comp> std::vector::const_iterator find(const std::vector &keys, const char *key) const { ensureCachePopulated(); const auto it = std::lower_bound(keys.begin(), keys.end(), key, Comp()); if (it == keys.end() || Comp()(*it, key)) { return it; } else { return keys.end(); } } template class Op> class Comp> std::vector::const_iterator find(const std::vector &keys, const char *key) const { ensureCachePopulated(); const auto it = std::lower_bound(keys.begin(), keys.end(), key, Comp()); if (it == keys.end() || Comp()(*it, key)) { return it; } else { return keys.end(); } } std::vector::const_iterator find_fpr(const char *fpr) const { return find<_detail::ByFingerprint>(by.fpr, fpr); } std::pair>::const_iterator, std::vector>::const_iterator> find_email(const char *email) const { ensureCachePopulated(); return std::equal_range(by.email.begin(), by.email.end(), email, ByEMail()); } std::vector find_mailbox(const QString &email, bool sign) const; std::vector::const_iterator find_keygrip(const char *keygrip) const { return find<_detail::ByKeyGrip>(by.keygrip, keygrip); } std::vector::const_iterator find_subkeyid(const char *subkeyid) const { return find<_detail::ByKeyID>(by.subkeyid, subkeyid); } std::vector::const_iterator find_keyid(const char *keyid) const { return find<_detail::ByKeyID>(by.keyid, keyid); } std::vector::const_iterator find_shortkeyid(const char *shortkeyid) const { return find<_detail::ByShortKeyID>(by.shortkeyid, shortkeyid); } std::pair::const_iterator, std::vector::const_iterator> find_subjects(const char *chain_id) const { ensureCachePopulated(); return std::equal_range(by.chainid.begin(), by.chainid.end(), chain_id, _detail::ByChainID()); } void refreshJobDone(const KeyListResult &result); void setRefreshInterval(int interval) { m_refreshInterval = interval; updateAutoKeyListingTimer(); } int refreshInterval() const { return m_refreshInterval; } void updateAutoKeyListingTimer() { setAutoKeyListingInterval(hours2ms * m_refreshInterval); } void setAutoKeyListingInterval(int ms) { m_autoKeyListingTimer.stop(); m_autoKeyListingTimer.setInterval(ms); if (ms != 0) { m_autoKeyListingTimer.start(); } } void ensureCachePopulated() const; void readGroupsFromGpgConf() { // According to Werner Koch groups are more of a hack to solve // a valid usecase (e.g. several keys defined for an internal mailing list) // that won't make it in the proper keylist interface. And using gpgconf // was the suggested way to support groups. auto conf = QGpgME::cryptoConfig(); if (!conf) { return; } auto entry = getCryptoConfigEntry(conf, "gpg", "group"); if (!entry) { return; } // collect the key fingerprints for all groups read from the configuration QMap fingerprints; const auto stringValueList = entry->stringValueList(); for (const QString &value : stringValueList) { const QStringList split = value.split(QLatin1Char('=')); if (split.size() != 2) { qCDebug(LIBKLEO_LOG) << "Ignoring invalid group config:" << value; continue; } const QString groupName = split[0]; const QString fingerprint = split[1]; fingerprints[groupName].push_back(fingerprint); } // add all groups read from the configuration to the list of groups for (auto it = fingerprints.cbegin(); it != fingerprints.cend(); ++it) { const QString groupName = it.key(); const std::vector groupKeys = q->findByFingerprint(toStdStrings(it.value())); KeyGroup g(groupName, groupName, groupKeys, KeyGroup::GnuPGConfig); m_groups.push_back(g); } } void readGroupsFromGroupsConfig() { Q_ASSERT(m_groupConfig); if (!m_groupConfig) { qCWarning(LIBKLEO_LOG) << __func__ << "group config not set"; return; } m_groups = m_groupConfig->readGroups(); } KeyGroup writeGroupToGroupsConfig(const KeyGroup &group) { Q_ASSERT(m_groupConfig); if (!m_groupConfig) { qCWarning(LIBKLEO_LOG) << __func__ << "group config not set"; return {}; } Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << __func__ << "group cannot be written to application configuration:" << group; return group; } return m_groupConfig->writeGroup(group); } bool removeGroupFromGroupsConfig(const KeyGroup &group) { Q_ASSERT(m_groupConfig); if (!m_groupConfig) { qCWarning(LIBKLEO_LOG) << __func__ << "group config not set"; return false; } Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << __func__ << "group cannot be removed from application configuration:" << group; return false; } return m_groupConfig->removeGroup(group); } void updateGroupCache() { // Update Group Keys // this is a quick thing as it only involves reading the config // so no need for a job. m_groups.clear(); if (m_groupsEnabled) { readGroupsFromGpgConf(); readGroupsFromGroupsConfig(); } } bool insert(const KeyGroup &group) { Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::insert - Invalid group:" << group; return false; } const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [group](const auto &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it != m_groups.cend()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::insert - Group already present in list of groups:" << group; return false; } const KeyGroup savedGroup = writeGroupToGroupsConfig(group); if (savedGroup.isNull()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::insert - Writing group" << group.id() << "to config file failed"; return false; } m_groups.push_back(savedGroup); Q_EMIT q->groupAdded(savedGroup); return true; } bool update(const KeyGroup &group) { Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::update - Invalid group:" << group; return false; } const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [group](const auto &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == m_groups.cend()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::update - Group not found in list of groups:" << group; return false; } const auto groupIndex = std::distance(m_groups.cbegin(), it); const KeyGroup savedGroup = writeGroupToGroupsConfig(group); if (savedGroup.isNull()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::update - Writing group" << group.id() << "to config file failed"; return false; } m_groups[groupIndex] = savedGroup; Q_EMIT q->groupUpdated(savedGroup); return true; } bool remove(const KeyGroup &group) { Q_ASSERT(!group.isNull()); Q_ASSERT(group.source() == KeyGroup::ApplicationConfig); if (group.isNull() || group.source() != KeyGroup::ApplicationConfig) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::remove - Invalid group:" << group; return false; } const auto it = std::find_if(m_groups.cbegin(), m_groups.cend(), [group](const auto &g) { return g.source() == group.source() && g.id() == group.id(); }); if (it == m_groups.cend()) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::remove - Group not found in list of groups:" << group; return false; } const bool success = removeGroupFromGroupsConfig(group); if (!success) { qCDebug(LIBKLEO_LOG) << "KeyCache::Private::remove - Removing group" << group.id() << "from config file failed"; return false; } m_groups.erase(it); Q_EMIT q->groupRemoved(group); return true; } private: QPointer m_refreshJob; std::vector> m_fsWatchers; QTimer m_autoKeyListingTimer; int m_refreshInterval; struct By { std::vector fpr, keyid, shortkeyid, chainid; std::vector> email; std::vector subkeyid, keygrip; } by; bool m_initalized; bool m_pgpOnly; bool m_remarks_enabled; bool m_groupsEnabled = false; std::shared_ptr m_groupConfig; std::vector m_groups; }; std::shared_ptr KeyCache::instance() { return mutableInstance(); } std::shared_ptr KeyCache::mutableInstance() { static std::weak_ptr self; try { return std::shared_ptr(self); } catch (const std::bad_weak_ptr &) { const std::shared_ptr s(new KeyCache); self = s; return s; } } KeyCache::KeyCache() : QObject() , d(new Private(this)) { } KeyCache::~KeyCache() { } void KeyCache::setGroupsEnabled(bool enabled) { d->m_groupsEnabled = enabled; if (d->m_initalized) { d->updateGroupCache(); } } void KeyCache::setGroupConfig(const std::shared_ptr &groupConfig) { d->m_groupConfig = groupConfig; } void KeyCache::enableFileSystemWatcher(bool enable) { for (const auto &i : std::as_const(d->m_fsWatchers)) { i->setEnabled(enable); } } void KeyCache::setRefreshInterval(int hours) { d->setRefreshInterval(hours); } int KeyCache::refreshInterval() const { return d->refreshInterval(); } std::shared_ptr KeyCache::suspendAutoRefresh() { return KeyCacheAutoRefreshSuspension::instance(); } void KeyCache::reload(GpgME::Protocol /*proto*/) { if (d->m_refreshJob) { return; } d->updateAutoKeyListingTimer(); enableFileSystemWatcher(false); d->m_refreshJob = new RefreshKeysJob(this); connect(d->m_refreshJob.data(), &RefreshKeysJob::done, this, [this](const GpgME::KeyListResult &r) { d->refreshJobDone(r); }); connect(d->m_refreshJob.data(), &RefreshKeysJob::canceled, this, [this]() { d->m_refreshJob.clear(); }); d->m_refreshJob->start(); } void KeyCache::cancelKeyListing() { if (!d->m_refreshJob) { return; } d->m_refreshJob->cancel(); } void KeyCache::addFileSystemWatcher(const std::shared_ptr &watcher) { if (!watcher) { return; } d->m_fsWatchers.push_back(watcher); connect(watcher.get(), &FileSystemWatcher::directoryChanged, this, [this]() { startKeyListing(); }); connect(watcher.get(), &FileSystemWatcher::fileChanged, this, [this]() { startKeyListing(); }); watcher->setEnabled(d->m_refreshJob.isNull()); } void KeyCache::enableRemarks(bool value) { if (!d->m_remarks_enabled && value) { d->m_remarks_enabled = value; if (d->m_initalized && !d->m_refreshJob) { qCDebug(LIBKLEO_LOG) << "Reloading keycache with remarks enabled"; reload(); } } else { d->m_remarks_enabled = value; } } bool KeyCache::remarksEnabled() const { return d->m_remarks_enabled; } void KeyCache::Private::refreshJobDone(const KeyListResult &result) { m_refreshJob.clear(); q->enableFileSystemWatcher(true); if (!m_initalized && q->remarksEnabled()) { // trigger another key listing to read signatures and signature notations QMetaObject::invokeMethod( q, [this]() { qCDebug(LIBKLEO_LOG) << "Reloading keycache with remarks enabled"; q->reload(); }, Qt::QueuedConnection); } m_initalized = true; updateGroupCache(); Q_EMIT q->keyListingDone(result); } const Key &KeyCache::findByFingerprint(const char *fpr) const { const std::vector::const_iterator it = d->find_fpr(fpr); if (it == d->by.fpr.end()) { static const Key null; return null; } else { return *it; } } const Key &KeyCache::findByFingerprint(const std::string &fpr) const { return findByFingerprint(fpr.c_str()); } std::vector KeyCache::findByFingerprint(const std::vector &fprs) const { std::vector keys; keys.reserve(fprs.size()); for (const auto &fpr : fprs) { const Key key = findByFingerprint(fpr.c_str()); if (key.isNull()) { qCDebug(LIBKLEO_LOG) << __func__ << "Ignoring unknown key with fingerprint:" << fpr.c_str(); continue; } keys.push_back(key); } return keys; } std::vector KeyCache::findByEMailAddress(const char *email) const { const auto pair = d->find_email(email); std::vector result; result.reserve(std::distance(pair.first, pair.second)); std::transform(pair.first, pair.second, std::back_inserter(result), [](const std::pair &pair) { return pair.second; }); return result; } std::vector KeyCache::findByEMailAddress(const std::string &email) const { return findByEMailAddress(email.c_str()); } const Key &KeyCache::findByShortKeyID(const char *id) const { const std::vector::const_iterator it = d->find_shortkeyid(id); if (it != d->by.shortkeyid.end()) { return *it; } static const Key null; return null; } const Key &KeyCache::findByShortKeyID(const std::string &id) const { return findByShortKeyID(id.c_str()); } const Key &KeyCache::findByKeyIDOrFingerprint(const char *id) const { { // try by.fpr first: const std::vector::const_iterator it = d->find_fpr(id); if (it != d->by.fpr.end()) { return *it; } } { // try by.keyid next: const std::vector::const_iterator it = d->find_keyid(id); if (it != d->by.keyid.end()) { return *it; } } static const Key null; return null; } const Key &KeyCache::findByKeyIDOrFingerprint(const std::string &id) const { return findByKeyIDOrFingerprint(id.c_str()); } std::vector KeyCache::findByKeyIDOrFingerprint(const std::vector &ids) const { std::vector keyids; std::remove_copy_if(ids.begin(), ids.end(), std::back_inserter(keyids), [](const std::string &str) { return !str.c_str() || !*str.c_str(); }); // this is just case-insensitive string search: std::sort(keyids.begin(), keyids.end(), _detail::ByFingerprint()); std::vector result; result.reserve(keyids.size()); // dups shouldn't happen d->ensureCachePopulated(); kdtools::set_intersection(d->by.fpr.begin(), d->by.fpr.end(), keyids.begin(), keyids.end(), std::back_inserter(result), _detail::ByFingerprint()); if (result.size() < keyids.size()) { // note that By{Fingerprint,KeyID,ShortKeyID} define the same // order for _strings_ kdtools::set_intersection(d->by.keyid.begin(), d->by.keyid.end(), keyids.begin(), keyids.end(), std::back_inserter(result), _detail::ByKeyID()); } // duplicates shouldn't happen, but make sure nonetheless: std::sort(result.begin(), result.end(), _detail::ByFingerprint()); result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); // we skip looking into short key ids here, as it's highly // unlikely they're used for this purpose. We might need to revise // this decision, but only after testing. return result; } const Subkey &KeyCache::findSubkeyByKeyGrip(const char *grip, Protocol protocol) const { static const Subkey null; d->ensureCachePopulated(); const auto range = std::equal_range(d->by.keygrip.begin(), d->by.keygrip.end(), grip, _detail::ByKeyGrip()); if (range.first == range.second) { return null; } else if (protocol == UnknownProtocol) { return *range.first; } else { for (auto it = range.first; it != range.second; ++it) { if (it->parent().protocol() == protocol) { return *it; } } } return null; } const Subkey &KeyCache::findSubkeyByKeyGrip(const std::string &grip, Protocol protocol) const { return findSubkeyByKeyGrip(grip.c_str(), protocol); } std::vector KeyCache::findSubkeysByKeyID(const std::vector &ids) const { std::vector sorted; sorted.reserve(ids.size()); std::remove_copy_if(ids.begin(), ids.end(), std::back_inserter(sorted), [](const std::string &str) { return !str.c_str() || !*str.c_str(); }); std::sort(sorted.begin(), sorted.end(), _detail::ByKeyID()); std::vector result; d->ensureCachePopulated(); kdtools::set_intersection(d->by.subkeyid.begin(), d->by.subkeyid.end(), sorted.begin(), sorted.end(), std::back_inserter(result), _detail::ByKeyID()); return result; } std::vector KeyCache::findRecipients(const DecryptionResult &res) const { std::vector keyids; const auto recipients = res.recipients(); for (const DecryptionResult::Recipient &r : recipients) { if (const char *kid = r.keyID()) { keyids.push_back(kid); } } const std::vector subkeys = findSubkeysByKeyID(keyids); std::vector result; result.reserve(subkeys.size()); std::transform(subkeys.begin(), subkeys.end(), std::back_inserter(result), std::mem_fn(&Subkey::parent)); std::sort(result.begin(), result.end(), _detail::ByFingerprint()); result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); return result; } std::vector KeyCache::findSigners(const VerificationResult &res) const { std::vector fprs; const auto signatures = res.signatures(); for (const Signature &s : signatures) { if (const char *fpr = s.fingerprint()) { fprs.push_back(fpr); } } return findByKeyIDOrFingerprint(fprs); } std::vector KeyCache::findSigningKeysByMailbox(const QString &mb) const { return d->find_mailbox(mb, true); } std::vector KeyCache::findEncryptionKeysByMailbox(const QString &mb) const { return d->find_mailbox(mb, false); } namespace { #define DO(op, meth, meth2) \ if (op key.meth()) { \ } else { \ qDebug("rejecting for signing: %s: %s", #meth2, key.primaryFingerprint()); \ return false; \ } #define ACCEPT(meth) DO(!!, meth, !meth) #define REJECT(meth) DO(!, meth, meth) struct ready_for_signing { bool operator()(const Key &key) const { ACCEPT(hasSecret); #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE ACCEPT(hasSign); #else ACCEPT(canSign); #endif REJECT(isRevoked); REJECT(isExpired); REJECT(isDisabled); REJECT(isInvalid); return true; #undef DO } }; #define DO(op, meth, meth2) \ if (op key.meth()) { \ } else { \ qDebug("rejecting for encrypting: %s: %s", #meth2, key.primaryFingerprint()); \ return false; \ } struct ready_for_encryption { bool operator()(const Key &key) const { #if 1 #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE ACCEPT(hasEncrypt); #else ACCEPT(canEncrypt); #endif REJECT(isRevoked); REJECT(isExpired); REJECT(isDisabled); REJECT(isInvalid); return true; #else return key.hasEncrypt() && !key.isRevoked() && !key.isExpired() && !key.isDisabled() && !key.isInvalid(); #endif } #undef DO #undef ACCEPT #undef REJECT }; } std::vector KeyCache::Private::find_mailbox(const QString &email, bool sign) const { if (email.isEmpty()) { return std::vector(); } const auto pair = find_email(email.toUtf8().constData()); std::vector result; result.reserve(std::distance(pair.first, pair.second)); if (sign) { kdtools::copy_2nd_if(pair.first, pair.second, std::back_inserter(result), ready_for_signing()); } else { kdtools::copy_2nd_if(pair.first, pair.second, std::back_inserter(result), ready_for_encryption()); } return result; } std::vector KeyCache::findSubjects(const GpgME::Key &key, Options options) const { + if (key.isNull()) { + return {}; + } + return findSubjects(std::vector(1, key), options); } std::vector KeyCache::findSubjects(const std::vector &keys, Options options) const { - return findSubjects(keys.begin(), keys.end(), options); -} + std::vector result; -std::vector KeyCache::findSubjects(std::vector::const_iterator first, std::vector::const_iterator last, Options options) const -{ - if (first == last) { - return std::vector(); + if (keys.empty()) { + return result; } - std::vector result; - while (first != last) { - const auto pair = d->find_subjects(first->primaryFingerprint()); - result.insert(result.end(), pair.first, pair.second); - ++first; + // get the immediate subjects + for (const auto &key : keys) { + const auto firstAndLastSubject = d->find_subjects(key.primaryFingerprint()); + result.insert(result.end(), firstAndLastSubject.first, firstAndLastSubject.second); } - - std::sort(result.begin(), result.end(), _detail::ByFingerprint()); - result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); + // remove duplicates + _detail::sort_by_fpr(result); + _detail::remove_duplicates_by_fpr(result); if (options & RecursiveSearch) { - const std::vector furtherSubjects = findSubjects(result, options); - std::vector combined; - combined.reserve(result.size() + furtherSubjects.size()); - std::merge(result.begin(), - result.end(), - furtherSubjects.begin(), - furtherSubjects.end(), - std::back_inserter(combined), - _detail::ByFingerprint()); - combined.erase(std::unique(combined.begin(), combined.end(), _detail::ByFingerprint()), combined.end()); - result.swap(combined); + for (std::vector furtherSubjects = findSubjects(result, NoOption); // + !furtherSubjects.empty(); + furtherSubjects = findSubjects(furtherSubjects, NoOption)) { + std::vector combined; + combined.reserve(result.size() + furtherSubjects.size()); + std::merge(result.begin(), + result.end(), + furtherSubjects.begin(), + furtherSubjects.end(), + std::back_inserter(combined), + _detail::ByFingerprint()); + _detail::remove_duplicates_by_fpr(combined); + if (result.size() == combined.size()) { + // no new subjects were found; this happens if a chain has a cycle + break; + } + result.swap(combined); + } } return result; } std::vector KeyCache::findIssuers(const Key &key, Options options) const { std::vector result; if (key.isNull()) { return result; } if (options & IncludeSubject) { result.push_back(key); } if (key.isRoot()) { return result; } - const Key &issuer = findByFingerprint(key.chainID()); + Key issuer = findByFingerprint(key.chainID()); if (issuer.isNull()) { return result; } result.push_back(issuer); if (!(options & RecursiveSearch)) { return result; } - while (true) { - const Key &issuer = findByFingerprint(result.back().chainID()); + while (!issuer.isRoot()) { + issuer = findByFingerprint(result.back().chainID()); if (issuer.isNull()) { break; } const bool chainAlreadyContainsIssuer = Kleo::contains_if(result, [issuer](const auto &key) { return _detail::ByFingerprint()(issuer, key); }); // we also add the issuer if the chain already contains it, so that - // the user can spot the recursion + // the user can spot the cycle result.push_back(issuer); - if (issuer.isRoot() || chainAlreadyContainsIssuer) { + if (chainAlreadyContainsIssuer) { + // break on cycle in chain break; } } return result; } static std::string email(const UserID &uid) { // Prefer the gnupg normalized one const std::string addr = uid.addrSpec(); if (!addr.empty()) { return addr; } const std::string email = uid.email(); if (email.empty()) { return DN(uid.id())[QStringLiteral("EMAIL")].trimmed().toUtf8().constData(); } if (email[0] == '<' && email[email.size() - 1] == '>') { return email.substr(1, email.size() - 2); } else { return email; } } static std::vector emails(const Key &key) { std::vector emails; const auto userIDs = key.userIDs(); for (const UserID &uid : userIDs) { const std::string e = email(uid); if (!e.empty()) { emails.push_back(e); } } std::sort(emails.begin(), emails.end(), ByEMail()); emails.erase(std::unique(emails.begin(), emails.end(), ByEMail()), emails.end()); return emails; } void KeyCache::remove(const Key &key) { if (key.isNull()) { return; } const char *fpr = key.primaryFingerprint(); if (!fpr) { return; } { const auto range = std::equal_range(d->by.fpr.begin(), d->by.fpr.end(), fpr, _detail::ByFingerprint()); d->by.fpr.erase(range.first, range.second); } if (const char *keyid = key.keyID()) { const auto range = std::equal_range(d->by.keyid.begin(), d->by.keyid.end(), keyid, _detail::ByKeyID()); const auto it = std::remove_if(range.first, range.second, [fpr](const GpgME::Key &key) { return _detail::ByFingerprint()(fpr, key); }); d->by.keyid.erase(it, range.second); } if (const char *shortkeyid = key.shortKeyID()) { const auto range = std::equal_range(d->by.shortkeyid.begin(), d->by.shortkeyid.end(), shortkeyid, _detail::ByShortKeyID()); const auto it = std::remove_if(range.first, range.second, [fpr](const GpgME::Key &key) { return _detail::ByFingerprint()(fpr, key); }); d->by.shortkeyid.erase(it, range.second); } if (const char *chainid = key.chainID()) { const auto range = std::equal_range(d->by.chainid.begin(), d->by.chainid.end(), chainid, _detail::ByChainID()); const auto range2 = std::equal_range(range.first, range.second, fpr, _detail::ByFingerprint()); d->by.chainid.erase(range2.first, range2.second); } const auto emailsKey{emails(key)}; for (const std::string &email : emailsKey) { const auto range = std::equal_range(d->by.email.begin(), d->by.email.end(), email, ByEMail()); const auto it = std::remove_if(range.first, range.second, [fpr](const std::pair &pair) { return qstricmp(fpr, pair.second.primaryFingerprint()) == 0; }); d->by.email.erase(it, range.second); } const auto keySubKeys{key.subkeys()}; for (const Subkey &subkey : keySubKeys) { if (const char *keyid = subkey.keyID()) { const auto range = std::equal_range(d->by.subkeyid.begin(), d->by.subkeyid.end(), keyid, _detail::ByKeyID()); const auto it = std::remove_if(range.first, range.second, [fpr](const Subkey &subkey) { return !qstricmp(fpr, subkey.parent().primaryFingerprint()); }); d->by.subkeyid.erase(it, range.second); } if (const char *keygrip = subkey.keyGrip()) { const auto range = std::equal_range(d->by.keygrip.begin(), d->by.keygrip.end(), keygrip, _detail::ByKeyGrip()); const auto it = std::remove_if(range.first, range.second, [fpr](const Subkey &subkey) { return !qstricmp(fpr, subkey.parent().primaryFingerprint()); }); d->by.keygrip.erase(it, range.second); } } } void KeyCache::remove(const std::vector &keys) { for (const Key &key : keys) { remove(key); } } const std::vector &KeyCache::keys() const { d->ensureCachePopulated(); return d->by.fpr; } std::vector KeyCache::secretKeys() const { std::vector keys = this->keys(); keys.erase(std::remove_if(keys.begin(), keys.end(), [](const Key &key) { return !key.hasSecret(); }), keys.end()); return keys; } KeyGroup KeyCache::group(const QString &id) const { KeyGroup result{}; const auto it = std::find_if(std::cbegin(d->m_groups), std::cend(d->m_groups), [id](const auto &g) { return g.id() == id; }); if (it != std::cend(d->m_groups)) { result = *it; } return result; } std::vector KeyCache::groups() const { d->ensureCachePopulated(); return d->m_groups; } std::vector KeyCache::configurableGroups() const { std::vector groups; groups.reserve(d->m_groups.size()); std::copy_if(d->m_groups.cbegin(), d->m_groups.cend(), std::back_inserter(groups), [](const KeyGroup &group) { return group.source() == KeyGroup::ApplicationConfig; }); return groups; } namespace { bool compareById(const KeyGroup &lhs, const KeyGroup &rhs) { return lhs.id() < rhs.id(); } std::vector sortedById(std::vector groups) { std::sort(groups.begin(), groups.end(), &compareById); return groups; } } void KeyCache::saveConfigurableGroups(const std::vector &groups) { const std::vector oldGroups = sortedById(configurableGroups()); const std::vector newGroups = sortedById(groups); { std::vector removedGroups; std::set_difference(oldGroups.begin(), oldGroups.end(), newGroups.begin(), newGroups.end(), std::back_inserter(removedGroups), &compareById); for (const auto &group : std::as_const(removedGroups)) { qCDebug(LIBKLEO_LOG) << "Removing group" << group; d->remove(group); } } { std::vector updatedGroups; std::set_intersection(newGroups.begin(), newGroups.end(), oldGroups.begin(), oldGroups.end(), std::back_inserter(updatedGroups), &compareById); for (const auto &group : std::as_const(updatedGroups)) { qCDebug(LIBKLEO_LOG) << "Updating group" << group; d->update(group); } } { std::vector addedGroups; std::set_difference(newGroups.begin(), newGroups.end(), oldGroups.begin(), oldGroups.end(), std::back_inserter(addedGroups), &compareById); for (const auto &group : std::as_const(addedGroups)) { qCDebug(LIBKLEO_LOG) << "Adding group" << group; d->insert(group); } } Q_EMIT keysMayHaveChanged(); } bool KeyCache::insert(const KeyGroup &group) { if (!d->insert(group)) { return false; } Q_EMIT keysMayHaveChanged(); return true; } bool KeyCache::update(const KeyGroup &group) { if (!d->update(group)) { return false; } Q_EMIT keysMayHaveChanged(); return true; } bool KeyCache::remove(const KeyGroup &group) { if (!d->remove(group)) { return false; } Q_EMIT keysMayHaveChanged(); return true; } void KeyCache::refresh(const std::vector &keys) { // make this better... clear(); insert(keys); } void KeyCache::insert(const Key &key) { insert(std::vector(1, key)); } namespace { template class Op> class T1, template class Op> class T2> struct lexicographically { using result_type = bool; template bool operator()(const U &lhs, const V &rhs) const { return T1()(lhs, rhs) // || (T1()(lhs, rhs) && T2()(lhs, rhs)); } }; } void KeyCache::insert(const std::vector &keys) { // 1. remove those with empty fingerprints: std::vector sorted; sorted.reserve(keys.size()); std::remove_copy_if(keys.begin(), keys.end(), std::back_inserter(sorted), [](const Key &key) { auto fp = key.primaryFingerprint(); return !fp || !*fp; }); Q_FOREACH (const Key &key, sorted) { remove(key); // this is sub-optimal, but makes implementation from here on much easier } // 2. sort by fingerprint: std::sort(sorted.begin(), sorted.end(), _detail::ByFingerprint()); // 2a. insert into fpr index: std::vector by_fpr; by_fpr.reserve(sorted.size() + d->by.fpr.size()); std::merge(sorted.begin(), sorted.end(), d->by.fpr.begin(), d->by.fpr.end(), std::back_inserter(by_fpr), _detail::ByFingerprint()); // 3. build email index: std::vector> pairs; pairs.reserve(sorted.size()); for (const Key &key : std::as_const(sorted)) { const std::vector emails = ::emails(key); for (const std::string &e : emails) { pairs.push_back(std::make_pair(e, key)); } } std::sort(pairs.begin(), pairs.end(), ByEMail()); // 3a. insert into email index: std::vector> by_email; by_email.reserve(pairs.size() + d->by.email.size()); std::merge(pairs.begin(), pairs.end(), d->by.email.begin(), d->by.email.end(), std::back_inserter(by_email), ByEMail()); // 3.5: stable-sort by chain-id (effectively lexicographically) std::stable_sort(sorted.begin(), sorted.end(), _detail::ByChainID()); // 3.5a: insert into chain-id index: std::vector nonroot; nonroot.reserve(sorted.size()); std::vector by_chainid; by_chainid.reserve(sorted.size() + d->by.chainid.size()); std::copy_if(sorted.cbegin(), sorted.cend(), std::back_inserter(nonroot), [](const Key &key) { return !key.isRoot(); }); std::merge(nonroot.cbegin(), nonroot.cend(), d->by.chainid.cbegin(), d->by.chainid.cend(), std::back_inserter(by_chainid), lexicographically<_detail::ByChainID, _detail::ByFingerprint>()); // 4. sort by key id: std::sort(sorted.begin(), sorted.end(), _detail::ByKeyID()); // 4a. insert into keyid index: std::vector by_keyid; by_keyid.reserve(sorted.size() + d->by.keyid.size()); std::merge(sorted.begin(), sorted.end(), d->by.keyid.begin(), d->by.keyid.end(), std::back_inserter(by_keyid), _detail::ByKeyID()); // 5. sort by short key id: std::sort(sorted.begin(), sorted.end(), _detail::ByShortKeyID()); // 5a. insert into short keyid index: std::vector by_shortkeyid; by_shortkeyid.reserve(sorted.size() + d->by.shortkeyid.size()); std::merge(sorted.begin(), sorted.end(), d->by.shortkeyid.begin(), d->by.shortkeyid.end(), std::back_inserter(by_shortkeyid), _detail::ByShortKeyID()); // 6. build subkey ID index: std::vector subkeys; subkeys.reserve(sorted.size()); for (const Key &key : std::as_const(sorted)) { const auto keySubkeys{key.subkeys()}; for (const Subkey &subkey : keySubkeys) { subkeys.push_back(subkey); } } // 6a sort by key id: std::sort(subkeys.begin(), subkeys.end(), _detail::ByKeyID()); // 6b. insert into subkey ID index: std::vector by_subkeyid; by_subkeyid.reserve(subkeys.size() + d->by.subkeyid.size()); std::merge(subkeys.begin(), subkeys.end(), d->by.subkeyid.begin(), d->by.subkeyid.end(), std::back_inserter(by_subkeyid), _detail::ByKeyID()); // 6c. sort by key grip std::sort(subkeys.begin(), subkeys.end(), _detail::ByKeyGrip()); // 6d. insert into subkey keygrip index: std::vector by_keygrip; by_keygrip.reserve(subkeys.size() + d->by.keygrip.size()); std::merge(subkeys.begin(), subkeys.end(), d->by.keygrip.begin(), d->by.keygrip.end(), std::back_inserter(by_keygrip), _detail::ByKeyGrip()); // now commit (well, we already removed keys...) by_fpr.swap(d->by.fpr); by_keyid.swap(d->by.keyid); by_shortkeyid.swap(d->by.shortkeyid); by_email.swap(d->by.email); by_subkeyid.swap(d->by.subkeyid); by_keygrip.swap(d->by.keygrip); by_chainid.swap(d->by.chainid); for (const Key &key : std::as_const(sorted)) { d->m_pgpOnly &= key.protocol() == GpgME::OpenPGP; } Q_EMIT keysMayHaveChanged(); } void KeyCache::clear() { d->by = Private::By(); } // // // RefreshKeysJob // // class KeyCache::RefreshKeysJob::Private { RefreshKeysJob *const q; public: Private(KeyCache *cache, RefreshKeysJob *qq); void doStart(); Error startKeyListing(GpgME::Protocol protocol); void listAllKeysJobDone(const KeyListResult &res, const std::vector &nextKeys) { std::vector keys; keys.reserve(m_keys.size() + nextKeys.size()); if (m_keys.empty()) { keys = nextKeys; } else { std::merge(m_keys.begin(), m_keys.end(), nextKeys.begin(), nextKeys.end(), std::back_inserter(keys), _detail::ByFingerprint()); } m_keys.swap(keys); jobDone(res); } void emitDone(const KeyListResult &result); void updateKeyCache(); QPointer m_cache; QList m_jobsPending; std::vector m_keys; KeyListResult m_mergedResult; bool m_canceled; private: void jobDone(const KeyListResult &res); }; KeyCache::RefreshKeysJob::Private::Private(KeyCache *cache, RefreshKeysJob *qq) : q(qq) , m_cache(cache) , m_canceled(false) { Q_ASSERT(m_cache); } void KeyCache::RefreshKeysJob::Private::jobDone(const KeyListResult &result) { if (m_canceled) { q->deleteLater(); return; } QObject *const sender = q->sender(); if (sender) { sender->disconnect(q); } Q_ASSERT(m_jobsPending.size() > 0); m_jobsPending.removeOne(qobject_cast(sender)); m_mergedResult.mergeWith(result); if (m_jobsPending.size() > 0) { return; } updateKeyCache(); emitDone(m_mergedResult); } void KeyCache::RefreshKeysJob::Private::emitDone(const KeyListResult &res) { q->deleteLater(); Q_EMIT q->done(res); } KeyCache::RefreshKeysJob::RefreshKeysJob(KeyCache *cache, QObject *parent) : QObject(parent) , d(new Private(cache, this)) { } KeyCache::RefreshKeysJob::~RefreshKeysJob() { delete d; } void KeyCache::RefreshKeysJob::start() { QTimer::singleShot(0, this, [this]() { d->doStart(); }); } void KeyCache::RefreshKeysJob::cancel() { d->m_canceled = true; std::for_each(d->m_jobsPending.begin(), d->m_jobsPending.end(), std::mem_fn(&QGpgME::ListAllKeysJob::slotCancel)); Q_EMIT canceled(); } void KeyCache::RefreshKeysJob::Private::doStart() { if (m_canceled) { q->deleteLater(); return; } Q_ASSERT(m_jobsPending.size() == 0); m_mergedResult.mergeWith(KeyListResult(startKeyListing(GpgME::OpenPGP))); m_mergedResult.mergeWith(KeyListResult(startKeyListing(GpgME::CMS))); if (m_jobsPending.size() != 0) { return; } const bool hasError = m_mergedResult.error() || m_mergedResult.error().isCanceled(); emitDone(hasError ? m_mergedResult : KeyListResult(Error(GPG_ERR_UNSUPPORTED_OPERATION))); } void KeyCache::RefreshKeysJob::Private::updateKeyCache() { if (!m_cache || m_canceled) { q->deleteLater(); return; } std::vector cachedKeys = m_cache->initialized() ? m_cache->keys() : std::vector(); std::sort(cachedKeys.begin(), cachedKeys.end(), _detail::ByFingerprint()); std::vector keysToRemove; std::set_difference(cachedKeys.begin(), cachedKeys.end(), m_keys.begin(), m_keys.end(), std::back_inserter(keysToRemove), _detail::ByFingerprint()); m_cache->remove(keysToRemove); m_cache->refresh(m_keys); } Error KeyCache::RefreshKeysJob::Private::startKeyListing(GpgME::Protocol proto) { const auto *const protocol = (proto == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); if (!protocol) { return Error(); } QGpgME::ListAllKeysJob *const job = protocol->listAllKeysJob(/*includeSigs*/ false, /*validate*/ true); if (!job) { return Error(); } if (!m_cache->initialized()) { // avoid delays during the initial key listing job->setOptions(QGpgME::ListAllKeysJob::DisableAutomaticTrustDatabaseCheck); } #if 0 aheinecke: 2017.01.12: For unknown reasons the new style connect fails at runtime over library borders into QGpgME from the GpgME repo when cross compiled for Windows and default arguments are used in the Signal. This was tested with gcc 4.9 (Mingw 3.0.2) and we could not find an explanation for this. So until this is fixed or we understand the problem we need to use the old style connect for QGpgME signals. The new style connect of the canceled signal right below works fine. connect(job, &QGpgME::ListAllKeysJob::result, q, [this](const GpgME::KeyListResult &res, const std::vector &keys) { listAllKeysJobDone(res, keys); }); #endif connect(job, SIGNAL(result(GpgME::KeyListResult, std::vector)), q, SLOT(listAllKeysJobDone(GpgME::KeyListResult, std::vector))); connect(q, &RefreshKeysJob::canceled, job, &QGpgME::Job::slotCancel); // Only do this for initialized keycaches to avoid huge waits for // signature notations during initial keylisting. if (proto == GpgME::OpenPGP && m_cache->remarksEnabled() && m_cache->initialized()) { auto ctx = QGpgME::Job::context(job); if (ctx) { ctx->addKeyListMode(KeyListMode::Signatures | KeyListMode::SignatureNotations); } } const Error error = job->start(true); if (!error && !error.isCanceled()) { m_jobsPending.push_back(job); } return error; } bool KeyCache::initialized() const { return d->m_initalized; } void KeyCache::Private::ensureCachePopulated() const { if (!m_initalized) { q->startKeyListing(); QEventLoop loop; loop.connect(q, &KeyCache::keyListingDone, &loop, &QEventLoop::quit); qCDebug(LIBKLEO_LOG) << "Waiting for keycache."; loop.exec(); qCDebug(LIBKLEO_LOG) << "Keycache available."; } } bool KeyCache::pgpOnly() const { return d->m_pgpOnly; } static bool keyIsOk(const Key &k) { return !k.isExpired() && !k.isRevoked() && !k.isInvalid() && !k.isDisabled(); } static bool uidIsOk(const UserID &uid) { return keyIsOk(uid.parent()) && !uid.isRevoked() && !uid.isInvalid(); } static bool subkeyIsOk(const Subkey &s) { return !s.isRevoked() && !s.isInvalid() && !s.isDisabled(); } namespace { time_t creationTimeOfNewestSuitableSubKey(const Key &key, KeyCache::KeyUsage usage) { time_t creationTime = 0; for (const Subkey &s : key.subkeys()) { if (!subkeyIsOk(s)) { continue; } if (usage == KeyCache::KeyUsage::Sign && !s.canSign()) { continue; } if (usage == KeyCache::KeyUsage::Encrypt && !s.canEncrypt()) { continue; } if (s.creationTime() > creationTime) { creationTime = s.creationTime(); } } return creationTime; } struct BestMatch { Key key; UserID uid; time_t creationTime = 0; }; } GpgME::Key KeyCache::findBestByMailBox(const char *addr, GpgME::Protocol proto, KeyUsage usage) const { d->ensureCachePopulated(); if (!addr) { return {}; } // support lookup of email addresses enclosed in angle brackets QByteArray address(addr); if (address.size() > 1 && address[0] == '<' && address[address.size() - 1] == '>') { address = address.mid(1, address.size() - 2); } address = address.toLower(); BestMatch best; for (const Key &k : findByEMailAddress(address.constData())) { if (proto != Protocol::UnknownProtocol && k.protocol() != proto) { continue; } if (usage == KeyUsage::Encrypt && !keyHasEncrypt(k)) { continue; } if (usage == KeyUsage::Sign && (!keyHasSign(k) || !k.hasSecret())) { continue; } const time_t creationTime = creationTimeOfNewestSuitableSubKey(k, usage); if (creationTime == 0) { // key does not have a suitable (and usable) subkey continue; } for (const UserID &u : k.userIDs()) { if (QByteArray::fromStdString(u.addrSpec()).toLower() != address) { // user ID does not match the given email address continue; } if (best.uid.isNull()) { // we have found our first candidate best = {k, u, creationTime}; } else if (!uidIsOk(best.uid) && uidIsOk(u)) { // validity of the new key is better best = {k, u, creationTime}; } else if (!k.isExpired() && best.uid.validity() < u.validity()) { // validity of the new key is better best = {k, u, creationTime}; } else if (best.key.isExpired() && !k.isExpired()) { // validity of the new key is better best = {k, u, creationTime}; } else if (best.uid.validity() == u.validity() && uidIsOk(u) && best.creationTime < creationTime) { // both keys/user IDs have same validity, but the new key is newer best = {k, u, creationTime}; } } } return best.key; } namespace { template bool allKeysAllowUsage(const T &keys, KeyCache::KeyUsage usage) { switch (usage) { case KeyCache::KeyUsage::AnyUsage: return true; case KeyCache::KeyUsage::Sign: return std::all_of(std::begin(keys), std::end(keys), #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE std::mem_fn(&Key::hasSign) #else Kleo::keyHasSign #endif ); case KeyCache::KeyUsage::Encrypt: return std::all_of(std::begin(keys), std::end(keys), #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE std::mem_fn(&Key::hasEncrypt) #else Kleo::keyHasEncrypt #endif ); case KeyCache::KeyUsage::Certify: return std::all_of(std::begin(keys), std::end(keys), #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE std::mem_fn(&Key::hasCertify) #else Kleo::keyHasCertify #endif ); case KeyCache::KeyUsage::Authenticate: return std::all_of(std::begin(keys), std::end(keys), #if GPGMEPP_KEY_HAS_HASCERTIFY_SIGN_ENCRYPT_AUTHENTICATE std::mem_fn(&Key::hasAuthenticate) #else Kleo::keyHasAuthenticate #endif ); } qCDebug(LIBKLEO_LOG) << __func__ << "called with invalid usage" << int(usage); return false; } } KeyGroup KeyCache::findGroup(const QString &name, Protocol protocol, KeyUsage usage) const { d->ensureCachePopulated(); Q_ASSERT(usage == KeyUsage::Sign || usage == KeyUsage::Encrypt); for (const auto &group : std::as_const(d->m_groups)) { if (group.name() == name) { const KeyGroup::Keys &keys = group.keys(); if (allKeysAllowUsage(keys, usage) && (protocol == UnknownProtocol || allKeysHaveProtocol(keys, protocol))) { return group; } } } return {}; } std::vector KeyCache::getGroupKeys(const QString &groupName) const { std::vector result; for (const KeyGroup &g : std::as_const(d->m_groups)) { if (g.name() == groupName) { const KeyGroup::Keys &keys = g.keys(); std::copy(keys.cbegin(), keys.cend(), std::back_inserter(result)); } } _detail::sort_by_fpr(result); _detail::remove_duplicates_by_fpr(result); return result; } void KeyCache::setKeys(const std::vector &keys) { // disable regular key listing and cancel running key listing setRefreshInterval(0); cancelKeyListing(); clear(); insert(keys); d->m_initalized = true; Q_EMIT keyListingDone(KeyListResult()); } void KeyCache::setGroups(const std::vector &groups) { Q_ASSERT(d->m_initalized && "Call setKeys() before setting groups"); d->m_groups = groups; Q_EMIT keysMayHaveChanged(); } #include "moc_keycache.cpp" #include "moc_keycache_p.cpp" diff --git a/src/models/keycache.h b/src/models/keycache.h index d8f8bf3ce..5d280cb58 100644 --- a/src/models/keycache.h +++ b/src/models/keycache.h @@ -1,217 +1,215 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keycache.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include #include #include #include namespace GpgME { class Key; class DecryptionResult; class VerificationResult; class KeyListResult; class Subkey; } namespace KMime { namespace Types { class Mailbox; } } namespace Kleo { class FileSystemWatcher; class KeyGroup; class KeyGroupConfig; class KeyCacheAutoRefreshSuspension; class KLEO_EXPORT KeyCache : public QObject { Q_OBJECT protected: explicit KeyCache(); public: enum class KeyUsage { AnyUsage, Sign, Encrypt, Certify, Authenticate, }; static std::shared_ptr instance(); static std::shared_ptr mutableInstance(); ~KeyCache() override; void setGroupsEnabled(bool enabled); void setGroupConfig(const std::shared_ptr &groupConfig); void insert(const GpgME::Key &key); void insert(const std::vector &keys); bool insert(const KeyGroup &group); void refresh(const std::vector &keys); bool update(const KeyGroup &group); void remove(const GpgME::Key &key); void remove(const std::vector &keys); bool remove(const KeyGroup &group); void addFileSystemWatcher(const std::shared_ptr &watcher); void enableFileSystemWatcher(bool enable); void setRefreshInterval(int hours); int refreshInterval() const; std::shared_ptr suspendAutoRefresh(); void enableRemarks(bool enable); bool remarksEnabled() const; const std::vector &keys() const; std::vector secretKeys() const; KeyGroup group(const QString &id) const; std::vector groups() const; std::vector configurableGroups() const; void saveConfigurableGroups(const std::vector &groups); const GpgME::Key &findByFingerprint(const char *fpr) const; const GpgME::Key &findByFingerprint(const std::string &fpr) const; std::vector findByFingerprint(const std::vector &fprs) const; std::vector findByEMailAddress(const char *email) const; std::vector findByEMailAddress(const std::string &email) const; /** Look through the cache and search for the best key for a mailbox. * * The best key is the key with a UID for the provided mailbox that * has the highest validity and a subkey that is capable for the given * usage. * If more then one key have a UID with the same validity * the most recently created key is taken. * * @returns the "best" key for the mailbox. */ GpgME::Key findBestByMailBox(const char *addr, GpgME::Protocol proto, KeyUsage usage) const; /** * Looks for a group named @a name which contains keys with protocol @a protocol * that are suitable for the usage @a usage. * * If @a protocol is GpgME::OpenPGP or GpgME::CMS, then only groups consisting of keys * matching this protocol are considered. Use @a protocol GpgME::UnknownProtocol to consider * any groups regardless of the protocol including mixed-protocol groups. * * If @a usage is not KeyUsage::AnyUsage, then only groups consisting of keys supporting this usage * are considered. * The validity of keys and the presence of a private key (necessary for signing, certification, and * authentication) is not taken into account. * * The first group that fulfills all conditions is returned. * * @returns a matching group or a null group if no matching group is found. */ KeyGroup findGroup(const QString &name, GpgME::Protocol protocol, KeyUsage usage) const; const GpgME::Key &findByShortKeyID(const char *id) const; const GpgME::Key &findByShortKeyID(const std::string &id) const; const GpgME::Key &findByKeyIDOrFingerprint(const char *id) const; const GpgME::Key &findByKeyIDOrFingerprint(const std::string &id) const; std::vector findByKeyIDOrFingerprint(const std::vector &ids) const; const GpgME::Subkey &findSubkeyByKeyGrip(const char *grip, GpgME::Protocol protocol = GpgME::UnknownProtocol) const; const GpgME::Subkey &findSubkeyByKeyGrip(const std::string &grip, GpgME::Protocol protocol = GpgME::UnknownProtocol) const; std::vector findSubkeysByKeyID(const std::vector &ids) const; std::vector findRecipients(const GpgME::DecryptionResult &result) const; std::vector findSigners(const GpgME::VerificationResult &result) const; std::vector findSigningKeysByMailbox(const QString &mb) const; std::vector findEncryptionKeysByMailbox(const QString &mb) const; /** Check for group keys. * * @returns A list of keys configured for groupName. Empty if no group cached.*/ std::vector getGroupKeys(const QString &groupName) const; enum Option { // clang-format off NoOption = 0, RecursiveSearch = 1, IncludeSubject = 2, // clang-format on }; Q_DECLARE_FLAGS(Options, Option) std::vector findSubjects(const GpgME::Key &key, Options option = RecursiveSearch) const; std::vector findSubjects(const std::vector &keys, Options options = RecursiveSearch) const; - std::vector - findSubjects(std::vector::const_iterator first, std::vector::const_iterator last, Options options = RecursiveSearch) const; std::vector findIssuers(const GpgME::Key &key, Options options = RecursiveSearch) const; /** Check if at least one keylisting was finished. */ bool initialized() const; /** Check if all keys have OpenPGP Protocol. */ bool pgpOnly() const; /** Set the keys the cache shall contain. Marks cache as initialized. Use for tests only. */ void setKeys(const std::vector &keys); void setGroups(const std::vector &groups); public Q_SLOTS: void clear(); void startKeyListing(GpgME::Protocol proto = GpgME::UnknownProtocol) { reload(proto); } void reload(GpgME::Protocol proto = GpgME::UnknownProtocol); void cancelKeyListing(); Q_SIGNALS: void keyListingDone(const GpgME::KeyListResult &result); void keysMayHaveChanged(); void groupAdded(const Kleo::KeyGroup &group); void groupUpdated(const Kleo::KeyGroup &group); void groupRemoved(const Kleo::KeyGroup &group); private: class RefreshKeysJob; class Private; QScopedPointer const d; }; } Q_DECLARE_OPERATORS_FOR_FLAGS(Kleo::KeyCache::Options) diff --git a/src/models/useridproxymodel.cpp b/src/models/useridproxymodel.cpp new file mode 100644 index 000000000..3b7efd05b --- /dev/null +++ b/src/models/useridproxymodel.cpp @@ -0,0 +1,189 @@ +/* + SPDX-FileCopyrightText: 2024 g10 Code GmbH + SPDX-FileContributor: Tobias Fella + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "useridproxymodel.h" + +#include "keylist.h" +#include "keylistmodel.h" +#include "kleo/keyfiltermanager.h" +#include "utils/formatting.h" +#include "utils/systeminfo.h" + +#include + +#include + +using namespace Kleo; + +UserIDProxyModel::UserIDProxyModel(QObject *parent) + : AbstractKeyListSortFilterProxyModel(parent) +{ +} + +static QVariant returnIfValid(const QColor &t) +{ + if (t.isValid()) { + return t; + } else { + return QVariant(); + } +} + +QModelIndex UserIDProxyModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + if (!sourceIndex.isValid()) { + return {}; + } + int row = 0; + for (int i = 0; i < sourceIndex.row(); i++) { + row += userIDsOfSourceRow(i); + } + return index(row, sourceIndex.column(), {}); +} + +QModelIndex UserIDProxyModel::mapToSource(const QModelIndex &proxyIndex) const +{ + if (!proxyIndex.isValid()) { + return {}; + } + return sourceModel()->index(sourceRowForProxyIndex(proxyIndex), proxyIndex.column(), {}); +} + +int UserIDProxyModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + int sum = 0; + for (int i = 0; i < sourceModel()->rowCount(); i++) { + sum += userIDsOfSourceRow(i); + } + return sum; +} + +QModelIndex UserIDProxyModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid()) { + return {}; + } + return createIndex(row, column, nullptr); +} + +QModelIndex UserIDProxyModel::parent(const QModelIndex &) const +{ + return {}; +} + +int UserIDProxyModel::columnCount(const QModelIndex &index) const +{ + return sourceModel()->columnCount(mapToSource(index)); +} + +QVariant UserIDProxyModel::data(const QModelIndex &index, int role) const +{ + const auto row = sourceRowForProxyIndex(index); + const auto offset = sourceOffsetForProxyIndex(index); + const auto model = dynamic_cast(sourceModel()); + const auto key = model->key(model->index(row, 0)); + if (key.isNull()) { + return AbstractKeyListSortFilterProxyModel::data(index, role); + } + const auto userId = key.userID(offset); + if ((role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::AccessibleTextRole)) { + if (index.column() == KeyList::Columns::PrettyName) { + auto name = QString::fromUtf8(userId.name()); + if (name.isEmpty()) { + return AbstractKeyListSortFilterProxyModel::data(index, role); + } + return name; + } + if (index.column() == KeyList::Columns::PrettyEMail) { + return QString::fromUtf8(userId.email()); + } + if (index.column() == KeyList::Columns::Validity) { + return Formatting::complianceStringShort(userId); + } + if (index.column() == KeyList::Columns::Summary) { + return Formatting::summaryLine(userId); + } + if (index.column() == KeyList::Columns::Origin) { + return Formatting::origin(userId.origin()); + } + if (index.column() == KeyList::Columns::LastUpdate) { + if (role == Qt::AccessibleTextRole) { + return Formatting::accessibleDate(userId.lastUpdate()); + } else { + return Formatting::dateString(userId.lastUpdate()); + } + } + } + if (role == Qt::BackgroundRole) { + if (!SystemInfo::isHighContrastModeActive()) { + return returnIfValid(KeyFilterManager::instance()->bgColor(userId)); + } + } else if (role == Qt::ForegroundRole) { + if (!SystemInfo::isHighContrastModeActive()) { + return returnIfValid(KeyFilterManager::instance()->fgColor(userId)); + } + } + return AbstractKeyListSortFilterProxyModel::data(index, role); +} + +int UserIDProxyModel::sourceRowForProxyIndex(const QModelIndex &index) const +{ + int row = index.row(); + int i; + for (i = 0; row >= userIDsOfSourceRow(i); i++) { + row -= userIDsOfSourceRow(i); + } + return i; +} + +int UserIDProxyModel::sourceOffsetForProxyIndex(const QModelIndex &index) const +{ + int row = index.row(); + int i; + for (i = 0; row >= userIDsOfSourceRow(i); i++) { + row -= userIDsOfSourceRow(i); + } + auto model = dynamic_cast(sourceModel()); + auto key = model->key(model->index(sourceRowForProxyIndex(index), 0)); + int tmp = row; + for (int j = 0; j <= tmp; j++) { + // account for filtered out S/MIME user IDs + if (key.protocol() == GpgME::Protocol::CMS && !key.userID(j).email()) { + row++; + } + } + return row; +} + +int UserIDProxyModel::userIDsOfSourceRow(int sourceRow) const +{ + auto model = dynamic_cast(sourceModel()); + auto key = model->key(model->index(sourceRow, 0)); + + if (key.protocol() == GpgME::OpenPGP) { + return key.numUserIDs(); + } + // Try to filter out some useless SMIME user ids + int count = 0; + const auto &uids = key.userIDs(); + for (auto it = uids.begin(); it != uids.end(); ++it) { + const auto &uid = *it; + if (uid.email()) { + count++; + } + } + return count; +} + +UserIDProxyModel *UserIDProxyModel::clone() const +{ + auto model = new UserIDProxyModel(QObject::parent()); + model->setSourceModel(sourceModel()); + return model; +} diff --git a/src/models/useridproxymodel.h b/src/models/useridproxymodel.h new file mode 100644 index 000000000..213c68afc --- /dev/null +++ b/src/models/useridproxymodel.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2024 g10 Code GmbH + SPDX-FileContributor: Tobias Fella + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kleo_export.h" +#include + +namespace Kleo +{ + +class KLEO_EXPORT UserIDProxyModel : public Kleo::AbstractKeyListSortFilterProxyModel +{ + Q_OBJECT + +public: + explicit UserIDProxyModel(QObject *parent = nullptr); + + QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + int rowCount(const QModelIndex &parent = {}) const override; + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + virtual QModelIndex parent(const QModelIndex &) const override; + int columnCount(const QModelIndex &) const override; + QVariant data(const QModelIndex &index, int role) const override; + int sourceRowForProxyIndex(const QModelIndex &index) const; + int sourceOffsetForProxyIndex(const QModelIndex &index) const; + int userIDsOfSourceRow(int sourceRow) const; + UserIDProxyModel *clone() const override; +}; +} diff --git a/src/ui/adjustingscrollarea.cpp b/src/ui/adjustingscrollarea.cpp new file mode 100644 index 000000000..cb14e260e --- /dev/null +++ b/src/ui/adjustingscrollarea.cpp @@ -0,0 +1,99 @@ +/* -*- mode: c++; c-basic-offset:4 -*- + ui/adjustingscrollarea.cpp + + This file is part of Kleopatra, the KDE keymanager + SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB + SPDX-FileCopyrightText: 2022 g10 Code GmbH + SPDX-FileContributor: Ingo Klöcker + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "adjustingscrollarea.h" + +#include +#include +#include +#include +#include + +using namespace Kleo; + +AdjustingScrollArea::AdjustingScrollArea(QWidget *parent) + : QScrollArea{parent} +{ + auto w = new QWidget; + w->setObjectName(QLatin1String("scrollarea_widget")); + new QVBoxLayout{w}; + setWidget(w); + setWidgetResizable(true); + w->installEventFilter(this); + + connect(qApp, &QApplication::focusChanged, this, [this](QWidget *old, QWidget *now) { + Q_UNUSED(old); + ensureWidgetVisible(now); + }); +} + +AdjustingScrollArea::~AdjustingScrollArea() +{ + widget()->removeEventFilter(this); +} + +QSize AdjustingScrollArea::minimumSizeHint() const +{ + const int fw = frameWidth(); + QSize sz{2 * fw, 2 * fw}; + sz += {widget()->minimumSizeHint().width(), 0}; + if (verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) { + sz.setWidth(sz.width() + verticalScrollBar()->sizeHint().width()); + } + if (horizontalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) { + sz.setHeight(sz.height() + horizontalScrollBar()->sizeHint().height()); + } + return QScrollArea::minimumSizeHint().expandedTo(sz); +} + +QSize AdjustingScrollArea::sizeHint() const +{ + const int fw = frameWidth(); + QSize sz{2 * fw, 2 * fw}; + sz += viewportSizeHint(); + if (verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) { + sz.setWidth(sz.width() + verticalScrollBar()->sizeHint().width()); + } + if (horizontalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) { + sz.setHeight(sz.height() + horizontalScrollBar()->sizeHint().height()); + } + sz = QScrollArea::sizeHint().expandedTo(sz); + return sz; +} + +void AdjustingScrollArea::adjustSizeOfWindowBy(const QSize &extent) +{ + if (auto w = window()) { + const auto currentSize = w->size(); + // we limit the automatic size adjustment to 2/3 of the screen's size + const auto maxWindowSize = screen()->geometry().size() * 2 / 3; + const auto newWindowSize = currentSize.expandedTo((currentSize + extent).boundedTo(maxWindowSize)); + if (newWindowSize != currentSize) { + w->resize(newWindowSize); + } + } +} + +bool AdjustingScrollArea::eventFilter(QObject *obj, QEvent *ev) +{ + if (ev->type() == QEvent::Resize && obj == widget() && sizeAdjustPolicy() == AdjustToContents) { + const auto *const event = static_cast(ev); + if (event->size().height() > event->oldSize().height()) { + const auto currentViewportHeight = viewport()->height(); + const auto wantedViewportHeight = event->size().height(); + const auto wantedAdditionalHeight = wantedViewportHeight - currentViewportHeight; + if (wantedAdditionalHeight > 0) { + adjustSizeOfWindowBy(QSize{0, wantedAdditionalHeight}); + } + } + } + return QScrollArea::eventFilter(obj, ev); +} diff --git a/src/ui/adjustingscrollarea.h b/src/ui/adjustingscrollarea.h new file mode 100644 index 000000000..2b048cf4d --- /dev/null +++ b/src/ui/adjustingscrollarea.h @@ -0,0 +1,56 @@ +/* -*- mode: c++; c-basic-offset:4 -*- + ui/adjustingscrollarea.h + + This file is part of Kleopatra, the KDE keymanager + SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB + SPDX-FileCopyrightText: 2022 g10 Code GmbH + SPDX-FileContributor: Ingo Klöcker + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace Kleo +{ + +/** + * This class improves a few aspects of QScrollArea for usage by us, in + * particular, for vertically scrollable widgets. + * + * If sizeAdjustPolicy is set to QAbstractScrollArea::AdjustToContents, + * then the scroll area will (try to) adjust its size to the widget to avoid + * scroll bars as much as possible. + */ +class AdjustingScrollArea : public QScrollArea +{ + Q_OBJECT + +public: + /** + * Creates a scroll area with a QWidget with QVBoxLayout that is flagged + * as resizable. + */ + explicit AdjustingScrollArea(QWidget *parent = nullptr); + ~AdjustingScrollArea() override; + + /** + * Reimplemented to add the minimum size hint of the widget. + */ + QSize minimumSizeHint() const override; + + /** + * Reimplemented to remove the caching of the size/size hint of the + * widget and to add the horizontal size hint of the vertical scroll bar + * unless it is explicitly turned off. + */ + QSize sizeHint() const override; + +private: + void adjustSizeOfWindowBy(const QSize &extent); + bool eventFilter(QObject *obj, QEvent *ev) override; +}; + +} diff --git a/src/ui/newkeyapprovaldialog.cpp b/src/ui/newkeyapprovaldialog.cpp index 4e5a41f50..16852de7a 100644 --- a/src/ui/newkeyapprovaldialog.cpp +++ b/src/ui/newkeyapprovaldialog.cpp @@ -1,934 +1,934 @@ /* -*- c++ -*- newkeyapprovaldialog.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2018 Intevation GmbH SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "newkeyapprovaldialog.h" #include "keyselectioncombo.h" #include "progressdialog.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; using namespace GpgME; namespace { class EncryptFilter : public DefaultKeyFilter { public: EncryptFilter() : DefaultKeyFilter() { setHasEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_encryptFilter = std::shared_ptr(new EncryptFilter); class OpenPGPFilter : public DefaultKeyFilter { public: OpenPGPFilter() : DefaultKeyFilter() { setIsOpenPGP(DefaultKeyFilter::Set); setHasEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_pgpEncryptFilter = std::shared_ptr(new OpenPGPFilter); class OpenPGPSignFilter : public DefaultKeyFilter { public: OpenPGPSignFilter() : DefaultKeyFilter() { /* Also list unusable keys to make it transparent why they are unusable */ setDisabled(DefaultKeyFilter::NotSet); setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanSign(DefaultKeyFilter::Set); setHasSecret(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::Set); } }; static std::shared_ptr s_pgpSignFilter = std::shared_ptr(new OpenPGPSignFilter); class SMIMEFilter : public DefaultKeyFilter { public: SMIMEFilter() : DefaultKeyFilter() { setIsOpenPGP(DefaultKeyFilter::NotSet); setHasEncrypt(DefaultKeyFilter::Set); } }; static std::shared_ptr s_smimeEncryptFilter = std::shared_ptr(new SMIMEFilter); class SMIMESignFilter : public DefaultKeyFilter { public: SMIMESignFilter() : DefaultKeyFilter() { setDisabled(DefaultKeyFilter::NotSet); setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setCanSign(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); } }; static std::shared_ptr s_smimeSignFilter = std::shared_ptr(new SMIMESignFilter); /* Some decoration and a button to remove the filter for a keyselectioncombo */ class ComboWidget : public QWidget { Q_OBJECT public: explicit ComboWidget(KeySelectionCombo *combo) : mCombo(combo) , mFilterBtn(new QPushButton) { auto hLay = new QHBoxLayout(this); auto infoBtn = new QPushButton; infoBtn->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); infoBtn->setIconSize(QSize(22, 22)); infoBtn->setFlat(true); infoBtn->setAccessibleName(i18nc("@action:button", "Show Details")); hLay->addWidget(infoBtn); hLay->addWidget(combo, 1); hLay->addWidget(mFilterBtn, 0); connect(infoBtn, &QPushButton::clicked, this, [this, infoBtn]() { QToolTip::showText(infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0), mCombo->currentData(Qt::ToolTipRole).toString(), infoBtn, QRect(), 30000); }); // FIXME: This is ugly to enforce but otherwise the // icon is broken. combo->setMinimumHeight(22); mFilterBtn->setMinimumHeight(23); updateFilterButton(); connect(mFilterBtn, &QPushButton::clicked, this, [this]() { const QString curFilter = mCombo->idFilter(); if (curFilter.isEmpty()) { setIdFilter(mLastIdFilter); mLastIdFilter = QString(); } else { setIdFilter(QString()); mLastIdFilter = curFilter; } }); } void setIdFilter(const QString &id) { mCombo->setIdFilter(id); updateFilterButton(); } void updateFilterButton() { if (mCombo->idFilter().isEmpty()) { mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-add-filters"))); mFilterBtn->setAccessibleName(i18nc("@action:button", "Show Matching Keys")); mFilterBtn->setToolTip(i18n("Show keys matching the email address")); } else { mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-remove-filters"))); mFilterBtn->setAccessibleName(i18nc("@action:button short for 'Show all keys'", "Show All")); mFilterBtn->setToolTip(i18n("Show all keys")); } } KeySelectionCombo *combo() { return mCombo; } GpgME::Protocol fixedProtocol() const { return mFixedProtocol; } void setFixedProtocol(GpgME::Protocol proto) { mFixedProtocol = proto; } private: KeySelectionCombo *mCombo; QPushButton *mFilterBtn; QString mLastIdFilter; GpgME::Protocol mFixedProtocol = GpgME::UnknownProtocol; }; static bool key_has_addr(const GpgME::Key &key, const QString &addr) { for (const auto &uid : key.userIDs()) { if (QString::fromStdString(uid.addrSpec()).toLower() == addr.toLower()) { return true; } } return false; } Key findfirstKeyOfType(const std::vector &keys, GpgME::Protocol protocol) { const auto it = std::find_if(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); return it != std::end(keys) ? *it : Key(); } } // namespace class NewKeyApprovalDialog::Private { private: enum Action { Unset, GenerateKey, IgnoreKey, }; public: enum { OpenPGPButtonId = 1, SMIMEButtonId = 2, }; Private(NewKeyApprovalDialog *qq, bool encrypt, bool sign, GpgME::Protocol forcedProtocol, GpgME::Protocol presetProtocol, const QString &sender, bool allowMixed) : mForcedProtocol{forcedProtocol} , mSender{sender} , mSign{sign} , mEncrypt{encrypt} , mAllowMixed{allowMixed} , q{qq} { Q_ASSERT(forcedProtocol == GpgME::UnknownProtocol || presetProtocol == GpgME::UnknownProtocol || presetProtocol == forcedProtocol); Q_ASSERT(!allowMixed || (allowMixed && forcedProtocol == GpgME::UnknownProtocol)); Q_ASSERT(!(!allowMixed && presetProtocol == GpgME::UnknownProtocol)); // We do the translation here to avoid having the same string multiple times. mGenerateTooltip = i18nc( "@info:tooltip for a 'Generate new key pair' action " "in a combobox when a user does not yet have an OpenPGP or S/MIME key.", "Generate a new key using your E-Mail address.

" "The key is necessary to decrypt and sign E-Mails. " "You will be asked for a passphrase to protect this key and the protected key " "will be stored in your home directory."); mMainLay = new QVBoxLayout; QDialogButtonBox *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); mOkButton = btnBox->button(QDialogButtonBox::Ok); #ifndef NDEBUG mOkButton->setObjectName(QLatin1StringView("ok button")); #endif QObject::connect(btnBox, &QDialogButtonBox::accepted, q, [this]() { accepted(); }); QObject::connect(btnBox, &QDialogButtonBox::rejected, q, &QDialog::reject); - mScrollArea = new QScrollArea; + mScrollArea = new AdjustingScrollArea; mScrollArea->setWidget(new QWidget); mScrollLayout = new QVBoxLayout; mScrollArea->widget()->setLayout(mScrollLayout); mScrollArea->setWidgetResizable(true); - mScrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow); + mScrollArea->setSizeAdjustPolicy(QScrollArea::AdjustToContents); mScrollArea->setFrameStyle(QFrame::NoFrame); mScrollLayout->setContentsMargins(0, 0, 0, 0); q->setWindowTitle(i18nc("@title:window", "Security approval")); auto fmtLayout = new QHBoxLayout; mFormatBtns = new QButtonGroup(qq); QAbstractButton *pgpBtn; QAbstractButton *smimeBtn; if (mAllowMixed) { pgpBtn = new QCheckBox(i18n("OpenPGP")); smimeBtn = new QCheckBox(i18n("S/MIME")); } else { pgpBtn = new QRadioButton(i18n("OpenPGP")); smimeBtn = new QRadioButton(i18n("S/MIME")); } #ifndef NDEBUG pgpBtn->setObjectName(QLatin1StringView("openpgp button")); smimeBtn->setObjectName(QLatin1StringView("smime button")); #endif mFormatBtns->addButton(pgpBtn, OpenPGPButtonId); mFormatBtns->addButton(smimeBtn, SMIMEButtonId); mFormatBtns->setExclusive(!mAllowMixed); connect(mFormatBtns, QOverload::of(&QButtonGroup::buttonClicked), q, [this]() { updateOkButton(); }); fmtLayout->addStretch(-1); fmtLayout->addWidget(pgpBtn); fmtLayout->addWidget(smimeBtn); mMainLay->addLayout(fmtLayout); if (mForcedProtocol != GpgME::UnknownProtocol) { pgpBtn->setChecked(mForcedProtocol == GpgME::OpenPGP); smimeBtn->setChecked(mForcedProtocol == GpgME::CMS); pgpBtn->setVisible(false); smimeBtn->setVisible(false); } else { pgpBtn->setChecked(presetProtocol == GpgME::OpenPGP || presetProtocol == GpgME::UnknownProtocol); smimeBtn->setChecked(presetProtocol == GpgME::CMS || presetProtocol == GpgME::UnknownProtocol); } QObject::connect(mFormatBtns, &QButtonGroup::idClicked, q, [this](int buttonId) { // ensure that at least one protocol button is checked if (mAllowMixed && !mFormatBtns->button(OpenPGPButtonId)->isChecked() && !mFormatBtns->button(SMIMEButtonId)->isChecked()) { mFormatBtns->button(buttonId == OpenPGPButtonId ? SMIMEButtonId : OpenPGPButtonId)->setChecked(true); } updateWidgets(); }); mMainLay->addWidget(mScrollArea); mComplianceLbl = new QLabel; mComplianceLbl->setVisible(false); #ifndef NDEBUG mComplianceLbl->setObjectName(QLatin1StringView("compliance label")); #endif auto btnLayout = new QHBoxLayout; btnLayout->addWidget(mComplianceLbl); btnLayout->addWidget(btnBox); mMainLay->addLayout(btnLayout); q->setLayout(mMainLay); } ~Private() = default; GpgME::Protocol currentProtocol() { const bool openPGPButtonChecked = mFormatBtns->button(OpenPGPButtonId)->isChecked(); const bool smimeButtonChecked = mFormatBtns->button(SMIMEButtonId)->isChecked(); if (mAllowMixed) { if (openPGPButtonChecked && !smimeButtonChecked) { return GpgME::OpenPGP; } if (!openPGPButtonChecked && smimeButtonChecked) { return GpgME::CMS; } } else if (openPGPButtonChecked) { return GpgME::OpenPGP; } else if (smimeButtonChecked) { return GpgME::CMS; } return GpgME::UnknownProtocol; } auto findVisibleKeySelectionComboWithGenerateKey() { const auto it = std::find_if(std::begin(mAllCombos), std::end(mAllCombos), [](auto combo) { return combo->isVisible() && combo->currentData(Qt::UserRole).toInt() == GenerateKey; }); return it != std::end(mAllCombos) ? *it : nullptr; } void generateKey(KeySelectionCombo *combo) { if (!mRunningJobs.empty()) { return; } const auto &addr = combo->property("address").toString(); auto job = QGpgME::openpgp()->quickJob(); auto progress = new Kleo::ProgressDialog(job, i18n("Generating key for '%1'...", addr) + QStringLiteral("\n\n") + i18n("This can take several minutes."), q); progress->setWindowFlags(progress->windowFlags() & ~Qt::WindowContextHelpButtonHint); progress->setWindowTitle(i18nc("@title:window", "Key generation")); progress->setModal(true); progress->setAutoClose(true); progress->setMinimumDuration(0); progress->setValue(0); mRunningJobs << job; if (!connect(job, &QGpgME::QuickJob::result, q, [this, job, combo]() { handleKeyGenResult(QGpgME::Job::context(job)->keyGenerationResult(), job, combo); })) { qCWarning(LIBKLEO_LOG) << "new-style connect failed; connecting to QGpgME::QuickJob::result the old way"; connect(job, SIGNAL(result(const GpgME::Error &)), q, SLOT(handleKeyGenResult())); } job->startCreate(addr, nullptr); return; } void handleKeyGenResult(const GpgME::KeyGenerationResult &result, QGpgME::Job *job, KeySelectionCombo *combo) { mLastError = result.error(); if (!mLastError) { connect(combo, &KeySelectionCombo::keyListingFinished, q, [this, job]() { mRunningJobs.removeAll(job); }); // update all combos showing the GenerateKey item for (auto c : std::as_const(mAllCombos)) { if (c->currentData(Qt::UserRole).toInt() == GenerateKey) { c->setDefaultKey(QString::fromLatin1(result.fingerprint()), GpgME::OpenPGP); c->refreshKeys(); } } } else { mRunningJobs.removeAll(job); } } void checkAccepted() { if (mLastError) { KMessageBox::error(q, Formatting::errorAsString(mLastError), i18n("Operation Failed")); mRunningJobs.clear(); return; } if (!mRunningJobs.empty()) { return; } /* Save the keys */ mAcceptedResult.protocol = currentProtocol(); for (const auto combo : std::as_const(mEncCombos)) { const auto addr = combo->property("address").toString(); const auto key = combo->currentKey(); if (!combo->isVisible() || key.isNull()) { continue; } mAcceptedResult.encryptionKeys[addr].push_back(key); } for (const auto combo : std::as_const(mSigningCombos)) { const auto key = combo->currentKey(); if (!combo->isVisible() || key.isNull()) { continue; } mAcceptedResult.signingKeys.push_back(key); } q->accept(); } void accepted() { // We can assume everything was validly resolved, otherwise // the OK button would have been disabled. // Handle custom items now. if (auto combo = findVisibleKeySelectionComboWithGenerateKey()) { generateKey(combo); return; } checkAccepted(); } auto encryptionKeyFilter(GpgME::Protocol protocol) { switch (protocol) { case OpenPGP: return s_pgpEncryptFilter; case CMS: return s_smimeEncryptFilter; default: return s_encryptFilter; } } void updateWidgets() { const GpgME::Protocol protocol = currentProtocol(); const auto encryptionFilter = encryptionKeyFilter(protocol); for (auto combo : std::as_const(mSigningCombos)) { auto widget = qobject_cast(combo->parentWidget()); if (!widget) { qCDebug(LIBKLEO_LOG) << "Failed to find signature combo widget"; continue; } widget->setVisible(protocol == GpgME::UnknownProtocol || widget->fixedProtocol() == GpgME::UnknownProtocol || widget->fixedProtocol() == protocol); } for (auto combo : std::as_const(mEncCombos)) { auto widget = qobject_cast(combo->parentWidget()); if (!widget) { qCDebug(LIBKLEO_LOG) << "Failed to find combo widget"; continue; } widget->setVisible(protocol == GpgME::UnknownProtocol || widget->fixedProtocol() == GpgME::UnknownProtocol || widget->fixedProtocol() == protocol); if (widget->isVisible() && combo->property("address") != mSender) { combo->setKeyFilter(encryptionFilter); } } // hide the labels indicating the protocol of the sender's keys if only a single protocol is active const auto protocolLabels = q->findChildren(QStringLiteral("protocol label")); for (auto label : protocolLabels) { label->setVisible(protocol == GpgME::UnknownProtocol); } } auto createProtocolLabel(GpgME::Protocol protocol) { auto label = new QLabel(Formatting::displayName(protocol)); label->setObjectName(QLatin1StringView("protocol label")); return label; } ComboWidget *createSigningCombo(const QString &addr, const GpgME::Key &key, GpgME::Protocol protocol = GpgME::UnknownProtocol) { Q_ASSERT(!key.isNull() || protocol != UnknownProtocol); protocol = !key.isNull() ? key.protocol() : protocol; auto combo = new KeySelectionCombo{true, KeyUsage::Sign}; auto comboWidget = new ComboWidget(combo); #ifndef NDEBUG combo->setObjectName(QLatin1StringView("signing key")); #endif if (protocol == GpgME::OpenPGP) { combo->setKeyFilter(s_pgpSignFilter); } else if (protocol == GpgME::CMS) { combo->setKeyFilter(s_smimeSignFilter); } if (key.isNull() || key_has_addr(key, mSender)) { comboWidget->setIdFilter(mSender); } comboWidget->setFixedProtocol(protocol); if (!key.isNull()) { combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), protocol); } if (key.isNull() && protocol == OpenPGP) { combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Generate a new key pair"), GenerateKey, mGenerateTooltip); } combo->appendCustomItem(Formatting::unavailableIcon(), i18n("Don't confirm identity and integrity"), IgnoreKey, i18nc("@info:tooltip for not selecting a key for signing.", "The E-Mail will not be cryptographically signed.")); mSigningCombos << combo; mAllCombos << combo; combo->setProperty("address", addr); connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this]() { updateOkButton(); }); connect(combo, qOverload(&QComboBox::currentIndexChanged), q, [this]() { updateOkButton(); }); return comboWidget; } void setSigningKeys(const std::vector &preferredKeys, const std::vector &alternativeKeys) { auto group = new QGroupBox(i18nc("Caption for signing key selection", "Confirm identity '%1' as:", mSender)); group->setAlignment(Qt::AlignLeft); auto sigLayout = new QVBoxLayout(group); const bool mayNeedOpenPGP = mForcedProtocol != CMS; const bool mayNeedCMS = mForcedProtocol != OpenPGP; if (mayNeedOpenPGP) { if (mAllowMixed) { sigLayout->addWidget(createProtocolLabel(OpenPGP)); } const Key preferredKey = findfirstKeyOfType(preferredKeys, OpenPGP); const Key alternativeKey = findfirstKeyOfType(alternativeKeys, OpenPGP); if (!preferredKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey; auto comboWidget = createSigningCombo(mSender, preferredKey); sigLayout->addWidget(comboWidget); } else if (!alternativeKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey; auto comboWidget = createSigningCombo(mSender, alternativeKey); sigLayout->addWidget(comboWidget); } else { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for OpenPGP key"; auto comboWidget = createSigningCombo(mSender, Key(), OpenPGP); sigLayout->addWidget(comboWidget); } } if (mayNeedCMS) { if (mAllowMixed) { sigLayout->addWidget(createProtocolLabel(CMS)); } const Key preferredKey = findfirstKeyOfType(preferredKeys, CMS); const Key alternativeKey = findfirstKeyOfType(alternativeKeys, CMS); if (!preferredKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey; auto comboWidget = createSigningCombo(mSender, preferredKey); sigLayout->addWidget(comboWidget); } else if (!alternativeKey.isNull()) { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey; auto comboWidget = createSigningCombo(mSender, alternativeKey); sigLayout->addWidget(comboWidget); } else { qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for S/MIME key"; auto comboWidget = createSigningCombo(mSender, Key(), CMS); sigLayout->addWidget(comboWidget); } } mScrollLayout->addWidget(group); } ComboWidget *createEncryptionCombo(const QString &addr, const GpgME::Key &key, GpgME::Protocol fixedProtocol) { auto combo = new KeySelectionCombo{false, KeyUsage::Encrypt}; auto comboWidget = new ComboWidget(combo); #ifndef NDEBUG combo->setObjectName(QLatin1StringView("encryption key")); #endif if (fixedProtocol == GpgME::OpenPGP) { combo->setKeyFilter(s_pgpEncryptFilter); } else if (fixedProtocol == GpgME::CMS) { combo->setKeyFilter(s_smimeEncryptFilter); } else { combo->setKeyFilter(s_encryptFilter); } if (key.isNull() || key_has_addr(key, addr)) { comboWidget->setIdFilter(addr); } comboWidget->setFixedProtocol(fixedProtocol); if (!key.isNull()) { combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), fixedProtocol); } if (addr == mSender && key.isNull() && fixedProtocol == OpenPGP) { combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Generate a new key pair"), GenerateKey, mGenerateTooltip); } combo->appendCustomItem(Formatting::unavailableIcon(), i18n("No key. Recipient will be unable to decrypt."), IgnoreKey, i18nc("@info:tooltip for No Key selected for a specific recipient.", "Do not select a key for this recipient.

" "The recipient will receive the encrypted E-Mail, but it can only " "be decrypted with the other keys selected in this dialog.")); connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this]() { updateOkButton(); }); connect(combo, qOverload(&QComboBox::currentIndexChanged), q, [this]() { updateOkButton(); }); mEncCombos << combo; mAllCombos << combo; combo->setProperty("address", addr); return comboWidget; } void addEncryptionAddr(const QString &addr, GpgME::Protocol preferredKeysProtocol, const std::vector &preferredKeys, GpgME::Protocol alternativeKeysProtocol, const std::vector &alternativeKeys, QGridLayout *encGrid) { if (addr == mSender) { const bool mayNeedOpenPGP = mForcedProtocol != CMS; const bool mayNeedCMS = mForcedProtocol != OpenPGP; if (mayNeedOpenPGP) { if (mAllowMixed) { encGrid->addWidget(createProtocolLabel(OpenPGP), encGrid->rowCount(), 0); } for (const auto &key : preferredKeys) { if (key.protocol() == OpenPGP) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, OpenPGP); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } for (const auto &key : alternativeKeys) { if (key.protocol() == OpenPGP) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, OpenPGP); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } if (!anyKeyHasProtocol(preferredKeys, OpenPGP) && !anyKeyHasProtocol(alternativeKeys, OpenPGP)) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for OpenPGP key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), OpenPGP); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } if (mayNeedCMS) { if (mAllowMixed) { encGrid->addWidget(createProtocolLabel(CMS), encGrid->rowCount(), 0); } for (const auto &key : preferredKeys) { if (key.protocol() == CMS) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, CMS); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } for (const auto &key : alternativeKeys) { if (key.protocol() == CMS) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, CMS); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } if (!anyKeyHasProtocol(preferredKeys, CMS) && !anyKeyHasProtocol(alternativeKeys, CMS)) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for S/MIME key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), CMS); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } } else { encGrid->addWidget(new QLabel(addr), encGrid->rowCount(), 0); for (const auto &key : preferredKeys) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, preferredKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } for (const auto &key : alternativeKeys) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key; auto comboWidget = createEncryptionCombo(addr, key, alternativeKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } if (!mAllowMixed) { if (preferredKeys.empty()) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(preferredKeysProtocol) << "key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), preferredKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } if (alternativeKeys.empty() && alternativeKeysProtocol != UnknownProtocol) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(alternativeKeysProtocol) << "key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), alternativeKeysProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } else { if (preferredKeys.empty() && alternativeKeys.empty()) { qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for any key"; auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), UnknownProtocol); encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2); } } } } void setEncryptionKeys(GpgME::Protocol preferredKeysProtocol, const QMap> &preferredKeys, GpgME::Protocol alternativeKeysProtocol, const QMap> &alternativeKeys) { { auto group = new QGroupBox(i18nc("Encrypt to self (email address):", "Encrypt to self (%1):", mSender)); #ifndef NDEBUG group->setObjectName(QLatin1StringView("encrypt-to-self box")); #endif group->setAlignment(Qt::AlignLeft); auto encGrid = new QGridLayout(group); addEncryptionAddr(mSender, preferredKeysProtocol, preferredKeys.value(mSender), alternativeKeysProtocol, alternativeKeys.value(mSender), encGrid); encGrid->setColumnStretch(1, -1); mScrollLayout->addWidget(group); } const bool hasOtherRecipients = std::any_of(preferredKeys.keyBegin(), preferredKeys.keyEnd(), [this](const auto &recipient) { return recipient != mSender; }); if (hasOtherRecipients) { auto group = new QGroupBox(i18n("Encrypt to others:")); #ifndef NDEBUG group->setObjectName(QLatin1StringView("encrypt-to-others box")); #endif group->setAlignment(Qt::AlignLeft); auto encGrid = new QGridLayout{group}; for (auto it = std::begin(preferredKeys); it != std::end(preferredKeys); ++it) { const auto &address = it.key(); const auto &keys = it.value(); if (address != mSender) { addEncryptionAddr(address, preferredKeysProtocol, keys, alternativeKeysProtocol, alternativeKeys.value(address), encGrid); } } encGrid->setColumnStretch(1, -1); mScrollLayout->addWidget(group); } mScrollLayout->addStretch(-1); } void updateOkButton() { static QString origOkText = mOkButton->text(); const bool isGenerate = bool(findVisibleKeySelectionComboWithGenerateKey()); const bool allVisibleEncryptionKeysAreIgnored = Kleo::all_of(mEncCombos, [](auto combo) { return !combo->isVisible() || combo->currentData(Qt::UserRole).toInt() == IgnoreKey; }); const bool allVisibleEncryptionKeysAreUsable = Kleo::all_of(mEncCombos, [](auto combo) { return !combo->isVisible() || combo->currentKey().isNull() || Kleo::canBeUsedForEncryption(combo->currentKey()); }); // If we don't encrypt, then the OK button is always enabled. Likewise, // if the "generate key" option is selected. Otherwise, // we only enable it if we encrypt to at least one recipient. mOkButton->setEnabled(isGenerate || !mEncrypt || (!allVisibleEncryptionKeysAreIgnored && allVisibleEncryptionKeysAreUsable)); mOkButton->setText(isGenerate ? i18n("Generate") : origOkText); if (!DeVSCompliance::isActive()) { return; } // Handle compliance bool de_vs = DeVSCompliance::isCompliant(); if (de_vs) { const GpgME::Protocol protocol = currentProtocol(); for (const auto combo : std::as_const(mAllCombos)) { if (!combo->isVisible()) { continue; } const auto key = combo->currentKey(); if (key.isNull()) { continue; } if (protocol != GpgME::UnknownProtocol && key.protocol() != protocol) { continue; } if (!DeVSCompliance::keyIsCompliant(key)) { de_vs = false; break; } } } DeVSCompliance::decorate(mOkButton, de_vs); mComplianceLbl->setText(DeVSCompliance::name(de_vs)); mComplianceLbl->setVisible(true); } GpgME::Protocol mForcedProtocol; QList mSigningCombos; QList mEncCombos; QList mAllCombos; QScrollArea *mScrollArea; QVBoxLayout *mScrollLayout; QPushButton *mOkButton; QVBoxLayout *mMainLay; QButtonGroup *mFormatBtns; QString mSender; bool mSign; bool mEncrypt; bool mAllowMixed; NewKeyApprovalDialog *q; QList mRunningJobs; GpgME::Error mLastError; QLabel *mComplianceLbl; KeyResolver::Solution mAcceptedResult; QString mGenerateTooltip; }; NewKeyApprovalDialog::NewKeyApprovalDialog(bool encrypt, bool sign, const QString &sender, KeyResolver::Solution preferredSolution, KeyResolver::Solution alternativeSolution, bool allowMixed, GpgME::Protocol forcedProtocol, QWidget *parent, Qt::WindowFlags f) : QDialog(parent, f) , d{std::make_unique(this, encrypt, sign, forcedProtocol, preferredSolution.protocol, sender, allowMixed)} { if (sign) { d->setSigningKeys(std::move(preferredSolution.signingKeys), std::move(alternativeSolution.signingKeys)); } if (encrypt) { d->setEncryptionKeys(allowMixed ? UnknownProtocol : preferredSolution.protocol, std::move(preferredSolution.encryptionKeys), allowMixed ? UnknownProtocol : alternativeSolution.protocol, std::move(alternativeSolution.encryptionKeys)); } d->updateWidgets(); d->updateOkButton(); const auto size = sizeHint(); const auto desk = screen()->size(); resize(QSize(desk.width() / 3, qMin(size.height(), desk.height() / 2))); } Kleo::NewKeyApprovalDialog::~NewKeyApprovalDialog() = default; KeyResolver::Solution NewKeyApprovalDialog::result() { return d->mAcceptedResult; } void NewKeyApprovalDialog::handleKeyGenResult() { if (d->mRunningJobs.empty()) { qCWarning(LIBKLEO_LOG) << __func__ << "No running job"; } const auto job = d->mRunningJobs.front(); const auto result = QGpgME::Job::context(job)->keyGenerationResult(); const auto combo = d->findVisibleKeySelectionComboWithGenerateKey(); d->handleKeyGenResult(result, job, combo); } #include "newkeyapprovaldialog.moc" #include "moc_newkeyapprovaldialog.cpp" diff --git a/src/utils/assuan.h b/src/utils/assuan.h index eee35e04d..ebfe580ce 100644 --- a/src/utils/assuan.h +++ b/src/utils/assuan.h @@ -1,72 +1,73 @@ /* utils/assuan.h This file is part of libkleopatra SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include +#include #include namespace GpgME { class AssuanTransaction; class Context; class DefaultAssuanTransaction; class Error; } namespace Kleo { /** The Assuan namespace collects functions for communicating with the GnuPG * agent via the Assuan protocol. */ namespace Assuan { /** Checks if the GnuPG agent is running and accepts connections. */ KLEO_EXPORT bool agentIsRunning(); /** Sends the Assuan @p command using the @p transaction and the @p assuanContext * to the GnuPG agent and waits for the result. The returned transaction can be used * to retrieve the result. * If an error occurred, then @p err provides details. */ KLEO_EXPORT std::unique_ptr sendCommand(std::shared_ptr &assuanContext, const std::string &command, std::unique_ptr transaction, GpgME::Error &err); /** Sends the Assuan @p command using a default Assuan transaction and the @p assuanContext * to the GnuPG agent and waits for the result. The returned transaction can be used * to retrieve the result. * If an error occurred, then @p err provides details. */ KLEO_EXPORT std::unique_ptr sendCommand(std::shared_ptr &assuanContext, const std::string &command, GpgME::Error &err); /** Sends the Assuan @p command using a default Assuan transaction and the @p assuanContext * to the GnuPG agent and waits for the result. Returns the data that was sent by * GnuPG agent in response to the @p command. * If an error occurred, then @p err provides details. */ KLEO_EXPORT std::string sendDataCommand(std::shared_ptr assuanContext, const std::string &command, GpgME::Error &err); /** Sends the Assuan @p command using a default Assuan transaction and the @p assuanContext * to the GnuPG agent and waits for the result. Returns the status lines that were sent by * GnuPG agent in response to the @p command. * If an error occurred, then @p err provides details. */ KLEO_EXPORT std::vector> sendStatusLinesCommand(std::shared_ptr assuanContext, const std::string &command, GpgME::Error &err); /** Sends the Assuan @p command using a default Assuan transaction and the @p assuanContext * to the GnuPG agent and waits for the result. Returns the status that was sent by * GnuPG agent in response to the @p command. * If an error occurred, then @p err provides details. */ KLEO_EXPORT std::string sendStatusCommand(const std::shared_ptr &assuanContext, const std::string &command, GpgME::Error &err); } } diff --git a/src/utils/classify.cpp b/src/utils/classify.cpp index ad19a4696..be2f11903 100644 --- a/src/utils/classify.cpp +++ b/src/utils/classify.cpp @@ -1,371 +1,375 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/classify.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 "classify.h" #include "algorithm.h" #include "classifyconfig.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo::Class; namespace { const unsigned int ExamineContentHint = 0x8000; static const QMap classifications{ // using QMap to keep ordering by extension which incidentally is also the prioritized order for outputFileExtension() {QStringLiteral("arl"), Kleo::Class::CMS | Binary | CertificateRevocationList}, {QStringLiteral("asc"), Kleo::Class::OpenPGP | Ascii | OpaqueSignature | DetachedSignature | CipherText | AnyCertStoreType | ExamineContentHint}, {QStringLiteral("cer"), Kleo::Class::CMS | Binary | Certificate}, {QStringLiteral("crl"), Kleo::Class::CMS | Binary | CertificateRevocationList}, {QStringLiteral("crt"), Kleo::Class::CMS | Binary | Certificate}, {QStringLiteral("der"), Kleo::Class::CMS | Binary | Certificate | CertificateRevocationList}, {QStringLiteral("eml"), Kleo::Class::MimeFile | Ascii}, {QStringLiteral("gpg"), Kleo::Class::OpenPGP | Binary | OpaqueSignature | CipherText | AnyCertStoreType | ExamineContentHint}, {QStringLiteral("mim"), Kleo::Class::MimeFile | Ascii}, {QStringLiteral("mime"), Kleo::Class::MimeFile | Ascii}, {QStringLiteral("mbox"), Kleo::Class::MimeFile | Ascii}, {QStringLiteral("p10"), Kleo::Class::CMS | Ascii | CertificateRequest}, {QStringLiteral("p12"), Kleo::Class::CMS | Binary | ExportedPSM}, {QStringLiteral("p7c"), Kleo::Class::CMS | Binary | Certificate}, {QStringLiteral("p7m"), Kleo::Class::CMS | AnyFormat | CipherText}, {QStringLiteral("p7s"), Kleo::Class::CMS | AnyFormat | AnySignature}, {QStringLiteral("pem"), Kleo::Class::CMS | Ascii | AnyType | ExamineContentHint}, {QStringLiteral("pfx"), Kleo::Class::CMS | Binary | Certificate}, {QStringLiteral("pgp"), Kleo::Class::OpenPGP | Binary | OpaqueSignature | CipherText | AnyCertStoreType | ExamineContentHint}, {QStringLiteral("sig"), Kleo::Class::OpenPGP | AnyFormat | DetachedSignature}, }; static const QHash gpgmeTypeMap{ // clang-format off {GpgME::Data::PGPSigned, Kleo::Class::OpenPGP | OpaqueSignature }, /* PGPOther might be just an unencrypted unsigned pgp message. Decrypt * would yield the plaintext anyway so for us this is CipherText. */ {GpgME::Data::PGPOther, Kleo::Class::OpenPGP | CipherText }, {GpgME::Data::PGPKey, Kleo::Class::OpenPGP | Certificate }, {GpgME::Data::CMSSigned, Kleo::Class::CMS | AnySignature }, {GpgME::Data::CMSEncrypted, Kleo::Class::CMS | CipherText }, /* See PGPOther */ {GpgME::Data::CMSOther, Kleo::Class::CMS | CipherText }, {GpgME::Data::X509Cert, Kleo::Class::CMS | Certificate }, {GpgME::Data::PKCS12, Kleo::Class::CMS | Binary | ExportedPSM }, {GpgME::Data::PGPEncrypted, Kleo::Class::OpenPGP | CipherText }, {GpgME::Data::PGPSignature, Kleo::Class::OpenPGP | DetachedSignature}, // clang-format on }; static const QSet mimeFileNames{ /* KMail standard name */ QStringLiteral("msg.asc"), QStringLiteral("smime.p7m"), - /* Old names of GpgOL attachments newer versions - * should use .mim file ending. */ QStringLiteral("openpgp-encrypted-message.asc"), + /* Old names of internal GpgOL attachments newer versions + * should use .mime file ending as it is connected with + * Kleopatra. */ QStringLiteral("GpgOL_MIME_structure.txt"), + QStringLiteral("GpgOL_MIME_structure.mime"), + /* This is gpgtools take on the filename */ + QStringLiteral("OpenPGP encrypted message.asc"), }; static const unsigned int defaultClassification = NoClass; template class asKeyValueRange { public: asKeyValueRange(T &data) : m_data{data} { } auto begin() { return m_data.keyValueBegin(); } auto end() { return m_data.keyValueEnd(); } private: T &m_data; }; } unsigned int Kleo::classify(const QStringList &fileNames) { if (fileNames.empty()) { return 0; } unsigned int result = classify(fileNames.front()); for (const QString &fileName : fileNames) { result &= classify(fileName); } return result; } static bool mimeTypeInherits(const QMimeType &mimeType, const QString &mimeTypeName) { // inherits is expensive on an invalid mimeType return mimeType.isValid() && mimeType.inherits(mimeTypeName); } /// Detect either a complete mail file (e.g. mbox or eml file) or a encrypted attachment /// corresponding to a mail file static bool isMailFile(const QFileInfo &fi) { static const QRegularExpression attachmentNumbering{QStringLiteral(R"(\([0-9]+\))")}; const auto fileName = fi.fileName().remove(attachmentNumbering); if (mimeFileNames.contains(fileName)) { return true; } { Kleo::ClassifyConfig classifyConfig; if (classifyConfig.p7mWithoutExtensionAreEmail() && fileName.endsWith(QStringLiteral(".p7m")) && fi.completeSuffix() == fi.suffix()) { // match "myfile.p7m" but not "myfile.pdf.p7m" return true; } } QMimeDatabase mimeDatabase; const auto mimeType = mimeDatabase.mimeTypeForFile(fi); return mimeTypeInherits(mimeType, QStringLiteral("message/rfc822")) || mimeTypeInherits(mimeType, QStringLiteral("application/mbox")); } static unsigned int classifyExtension(const QFileInfo &fi) { return classifications.value(fi.suffix(), defaultClassification); } unsigned int Kleo::classify(const QString &filename) { const QFileInfo fi(filename); if (!fi.exists()) { return 0; } if (isMailFile(fi)) { return Kleo::Class::MimeFile | Ascii; } QFile file(filename); /* The least reliable but always available classification */ const unsigned int extClass = classifyExtension(fi); if (!file.open(QIODevice::ReadOnly)) { qCDebug(LIBKLEO_LOG) << "Failed to open file: " << filename << " for classification."; return extClass; } /* More reliable */ const unsigned int contentClass = classifyContent(file.read(4096)); if (contentClass != defaultClassification) { qCDebug(LIBKLEO_LOG) << "Classified based on content as:" << contentClass; return contentClass; } /* Probably some X509 Stuff that GpgME in its wisdom does not handle. Again * file extension is probably more reliable as the last resort. */ qCDebug(LIBKLEO_LOG) << "No classification based on content."; return extClass; } unsigned int Kleo::classifyContent(const QByteArray &data) { QGpgME::QByteArrayDataProvider dp(data); GpgME::Data gpgmeData(&dp); GpgME::Data::Type type = gpgmeData.type(); return gpgmeTypeMap.value(type, defaultClassification); } QString Kleo::printableClassification(unsigned int classification) { QStringList parts; if (classification & Kleo::Class::CMS) { parts.push_back(QStringLiteral("CMS")); } if (classification & Kleo::Class::OpenPGP) { parts.push_back(QStringLiteral("OpenPGP")); } if (classification & Kleo::Class::Binary) { parts.push_back(QStringLiteral("Binary")); } if (classification & Kleo::Class::Ascii) { parts.push_back(QStringLiteral("Ascii")); } if (classification & Kleo::Class::DetachedSignature) { parts.push_back(QStringLiteral("DetachedSignature")); } if (classification & Kleo::Class::OpaqueSignature) { parts.push_back(QStringLiteral("OpaqueSignature")); } if (classification & Kleo::Class::ClearsignedMessage) { parts.push_back(QStringLiteral("ClearsignedMessage")); } if (classification & Kleo::Class::CipherText) { parts.push_back(QStringLiteral("CipherText")); } if (classification & Kleo::Class::Certificate) { parts.push_back(QStringLiteral("Certificate")); } if (classification & Kleo::Class::ExportedPSM) { parts.push_back(QStringLiteral("ExportedPSM")); } if (classification & Kleo::Class::CertificateRequest) { parts.push_back(QStringLiteral("CertificateRequest")); } if (classification & Kleo::Class::MimeFile) { parts.push_back(QStringLiteral("MimeFile")); } return parts.join(QLatin1String(", ")); } /*! \return the data file that corresponds to the signature file \a signatureFileName, or QString(), if no such file can be found. */ QString Kleo::findSignedData(const QString &signatureFileName) { if (!mayBeDetachedSignature(signatureFileName)) { return QString(); } const QFileInfo fi{signatureFileName}; const QString baseName = signatureFileName.chopped(fi.suffix().size() + 1); return QFile::exists(baseName) ? baseName : QString(); } /*! \return all (existing) candidate signature files for \a signedDataFileName Note that there can very well be more than one such file, e.g. if the same data file was signed by both CMS and OpenPGP certificates. */ QStringList Kleo::findSignatures(const QString &signedDataFileName) { QStringList result; for (const auto &[extension, classification] : asKeyValueRange(classifications)) { if (classification & DetachedSignature) { const QString candidate = signedDataFileName + QLatin1Char('.') + extension; if (QFile::exists(candidate)) { result.push_back(candidate); } } } return result; } #ifdef Q_OS_WIN static QString stripOutlookAttachmentNumbering(const QString &s) { static const QRegularExpression attachmentNumbering{QStringLiteral(R"(\s\([0-9]+\)$)")}; return QString{s}.remove(attachmentNumbering); } #endif /*! \return the (likely) output filename for \a inputFileName, or "inputFileName.out" if none can be determined. */ QString Kleo::outputFileName(const QString &inputFileName) { const QFileInfo fi(inputFileName); const QString suffix = fi.suffix(); if (classifications.find(suffix) == std::cend(classifications)) { return inputFileName + QLatin1String(".out"); } else { #ifdef Q_OS_WIN return stripOutlookAttachmentNumbering(inputFileName.chopped(suffix.size() + 1)); #else return inputFileName.chopped(suffix.size() + 1); #endif } } /*! \return the commonly used extension for files of type \a classification, or NULL if none such exists. */ QString Kleo::outputFileExtension(unsigned int classification, bool usePGPFileExt) { if (usePGPFileExt && (classification & Class::OpenPGP) && (classification & Class::Binary)) { return QStringLiteral("pgp"); } for (const auto &[extension, classification_] : asKeyValueRange(classifications)) { if ((classification_ & classification) == classification) { return extension; } } return {}; } bool Kleo::isFingerprint(const QString &fpr) { static QRegularExpression fprRegex(QStringLiteral("[0-9a-fA-F]{40}")); return fprRegex.match(fpr).hasMatch(); } bool Kleo::isChecksumFile(const QString &file) { static bool initialized; static QList patterns; const QFileInfo fi(file); if (!fi.exists()) { return false; } if (!initialized) { const auto getChecksumDefinitions = ChecksumDefinition::getChecksumDefinitions(); for (const std::shared_ptr &cd : getChecksumDefinitions) { if (cd) { const auto patternsList = cd->patterns(); for (const QString &pattern : patternsList) { #ifdef Q_OS_WIN patterns << QRegularExpression(QRegularExpression::anchoredPattern(pattern), QRegularExpression::CaseInsensitiveOption); #else patterns << QRegularExpression(QRegularExpression::anchoredPattern(pattern)); #endif } } } initialized = true; } const QString fileName = fi.fileName(); for (const QRegularExpression &pattern : std::as_const(patterns)) { if (pattern.match(fileName).hasMatch()) { return true; } } return false; } diff --git a/src/utils/compliance.cpp b/src/utils/compliance.cpp index 3c9f3c901..722d31262 100644 --- a/src/utils/compliance.cpp +++ b/src/utils/compliance.cpp @@ -1,150 +1,161 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/compliance.cpp This file is part of libkleopatra SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "compliance.h" #include "algorithm.h" #include "cryptoconfig.h" #include "gnupg.h" #include "keyhelpers.h" #include "stringutils.h" #include "systeminfo.h" #include #include #include #include #include #include #include bool Kleo::DeVSCompliance::isActive() { return getCryptoConfigStringValue("gpg", "compliance") == QLatin1String{"de-vs"}; } bool Kleo::DeVSCompliance::isCompliant() { if (!isActive()) { return false; } // The pseudo option compliance_de_vs was fully added in 2.2.34; // For versions between 2.2.28 and 2.2.33 there was a broken config // value with a wrong type. So for them we add an extra check. This // can be removed in future versions because for GnuPG we could assume // non-compliance for older versions as versions of Kleopatra for // which this matters are bundled with new enough versions of GnuPG anyway. if (engineIsVersion(2, 2, 28) && !engineIsVersion(2, 2, 34)) { return true; } return getCryptoConfigIntValue("gpg", "compliance_de_vs", 0) != 0; } bool Kleo::DeVSCompliance::algorithmIsCompliant(std::string_view algo) { return !isActive() || Kleo::contains(compliantAlgorithms(), algo); } bool Kleo::DeVSCompliance::allSubkeysAreCompliant(const GpgME::Key &key) { if (!isActive()) { return true; } // there is at least one usable subkey const auto usableSubkeys = Kleo::count_if(key.subkeys(), [](const auto &sub) { return !sub.isExpired() && !sub.isRevoked(); }); if (usableSubkeys == 0) { qCDebug(LIBKLEO_LOG) << __func__ << "No usable subkeys found for key" << key; return false; } // and all usable subkeys are compliant return Kleo::all_of(key.subkeys(), [](const auto &sub) { return sub.isDeVs() || sub.isExpired() || sub.isRevoked(); }); } +bool Kleo::DeVSCompliance::userIDIsCompliant(const GpgME::UserID &id) +{ + if (!isActive()) { + return true; + } + return (id.parent().keyListMode() & GpgME::Validate) // + && !id.isRevoked() // + && id.validity() >= GpgME::UserID::Full // + && allSubkeysAreCompliant(id.parent()); +} + bool Kleo::DeVSCompliance::keyIsCompliant(const GpgME::Key &key) { if (!isActive()) { return true; } return (key.keyListMode() & GpgME::Validate) // && allUserIDsHaveFullValidity(key) // && allSubkeysAreCompliant(key); } const std::vector &Kleo::DeVSCompliance::compliantAlgorithms() { static const std::vector compliantAlgos = { "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", "rsa3072", "rsa4096", }; return isActive() ? compliantAlgos : Kleo::availableAlgorithms(); } const std::vector &Kleo::DeVSCompliance::preferredCompliantAlgorithms() { static std::vector result; if (result.empty()) { const auto &preferredAlgos = Kleo::preferredAlgorithms(); result.reserve(preferredAlgos.size()); Kleo::copy_if(preferredAlgos, std::back_inserter(result), Kleo::DeVSCompliance::algorithmIsCompliant); } return result; } void Kleo::DeVSCompliance::decorate(QPushButton *button) { decorate(button, isCompliant()); } void Kleo::DeVSCompliance::decorate(QPushButton *button, bool compliant) { if (!button) { return; } if (compliant) { button->setIcon(QIcon::fromTheme(QStringLiteral("security-high"))); if (!SystemInfo::isHighContrastModeActive()) { const auto bgColor = KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::PositiveBackground).color().name(); button->setStyleSheet(QStringLiteral("QPushButton { background-color: %1; };").arg(bgColor)); } } else { button->setIcon(QIcon::fromTheme(QStringLiteral("security-medium"))); if (!SystemInfo::isHighContrastModeActive()) { const auto bgColor = KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NegativeBackground).color().name(); button->setStyleSheet(QStringLiteral("QPushButton { background-color: %1; };").arg(bgColor)); } } } QString Kleo::DeVSCompliance::name() { return name(isCompliant()); } QString Kleo::DeVSCompliance::name(bool compliant) { const auto filterId = compliant ? QStringLiteral("de-vs-filter") : QStringLiteral("not-de-vs-filter"); if (auto filter = KeyFilterManager::instance()->keyFilterByID(filterId)) { return filter->name(); } return compliant ? i18n("VS-NfD compliant") : i18n("Not VS-NfD compliant"); } diff --git a/src/utils/compliance.h b/src/utils/compliance.h index 7344f5bae..9f1fd7077 100644 --- a/src/utils/compliance.h +++ b/src/utils/compliance.h @@ -1,114 +1,123 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/compliance.h This file is part of libkleopatra SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include class QPushButton; class QString; namespace GpgME { class Key; +class UserID; } namespace Kleo::DeVSCompliance { /** * Returns true, if compliance mode "de-vs" is configured for GnuPG. * Note: It does not check whether the used GnuPG is actually compliant. */ KLEO_EXPORT bool isActive(); /** * Returns true, if compliance mode "de-vs" is configured for GnuPG and if * GnuPG passes a basic compliance check, i.e. at least libgcrypt and the used * RNG are compliant. */ KLEO_EXPORT bool isCompliant(); /** * Returns true, if the given algorithm is compliant with compliance mode * "de-vs". Always returns true, if compliance mode "de-vs" is not active. */ KLEO_EXPORT bool algorithmIsCompliant(std::string_view algo); /** * Returns true, if all usable subkeys of the key \p key are compliant with * compliance mode "de-vs". Usable subkeys are those that are neither revoked * nor expired. If the key doesn't have any usable subkeys, then false is * returned. * Always returns true, if compliance mode "de-vs" is not active. */ KLEO_EXPORT bool allSubkeysAreCompliant(const GpgME::Key &key); +/** + * Returns true, if the key \p key is compliant with compliance mode "de-vs". + * This function behaves like DeVSCompliance::keyIsCompliant, but only considers + * user id \p id; all other user ids are ignored. + * \see keyIsCompliant + */ +bool userIDIsCompliant(const GpgME::UserID &id); + /** * Returns true, if the key \p key is compliant with compliance mode "de-vs". * A key is considered compliant if all usable subkeys are compliant and if * all not revoked user IDs have at least full validity. The second condition * requires that the key has been validated. * Always returns true, if compliance mode "de-vs" is not active. * * \see allSubkeysAreCompliant */ KLEO_EXPORT bool keyIsCompliant(const GpgME::Key &key); /** * Returns a static list of the available compliant algorithms. */ KLEO_EXPORT const std::vector &compliantAlgorithms(); /** * Returns a static list of the preferred compliant algorithms with decreasing * preference. * Can be used to determine the default algorithm for generating new keys. */ KLEO_EXPORT const std::vector &preferredCompliantAlgorithms(); /** * \overload * * Sets the appropriate icon and, unless high-contrast mode is active, the * appropriate background color of \p button depending on the state of * compliance. */ KLEO_EXPORT void decorate(QPushButton *button); /** * Sets the appropriate icon and, unless high-contrast mode is active, the * appropriate background color of \p button depending on the value of * \p compliant. */ KLEO_EXPORT void decorate(QPushButton *button, bool compliant); /** * \overload * * Returns a localized name for the compliance or non-compliance depending on * the state of compliance. */ KLEO_EXPORT QString name(); /** * Returns a localized name for the compliance or non-compliance depending on * the value of \p compliant. * * \note The localized name is taken from the de-vs-filter filter resp. the * not-de-vs-filter. This allows the customization of the name for different * users because VS-NfD compliance is called differently in different * environments, e.g. NATO RESTRICTED or EU RESTRICTED. */ KLEO_EXPORT QString name(bool compliant); } diff --git a/src/utils/formatting.cpp b/src/utils/formatting.cpp index c8382aef5..28915ddc4 100644 --- a/src/utils/formatting.cpp +++ b/src/utils/formatting.cpp @@ -1,1399 +1,1475 @@ /* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*- utils/formatting.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "formatting.h" #include "algorithm.h" #include "compat.h" #include "compliance.h" #include "cryptoconfig.h" #include "gnupg.h" #include "keyhelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for Qt::escape #include #include #include using namespace GpgME; using namespace Kleo; namespace { QIcon iconForValidityAndCompliance(UserID::Validity validity, bool isCompliant) { switch (validity) { case UserID::Ultimate: case UserID::Full: case UserID::Marginal: return isCompliant ? Formatting::successIcon() : Formatting::infoIcon(); case UserID::Never: return Formatting::errorIcon(); case UserID::Undefined: case UserID::Unknown: default: return Formatting::infoIcon(); } } QIcon iconForValidity(const UserID &userId) { const bool keyIsCompliant = !DeVSCompliance::isActive() || // (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(userId.parent())); return iconForValidityAndCompliance(userId.validity(), keyIsCompliant); } } QIcon Formatting::IconProvider::icon(const GpgME::Key &key) const { if (usage.canEncrypt() && !Kleo::canBeUsedForEncryption(key)) { return Formatting::errorIcon(); } if (usage.canSign() && !Kleo::canBeUsedForSigning(key)) { return Formatting::errorIcon(); } if (key.isBad()) { return Formatting::errorIcon(); } const auto primaryUserId = key.userID(0); if (Kleo::isRevokedOrExpired(primaryUserId)) { return Formatting::errorIcon(); } return iconForValidity(primaryUserId); } QIcon Formatting::IconProvider::icon(const KeyGroup &group) const { if (usage.canEncrypt() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForEncryption)) { return Formatting::errorIcon(); } if (usage.canSign() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForSigning)) { return Formatting::errorIcon(); } return validityIcon(group); } QIcon Formatting::successIcon() { return QIcon::fromTheme(QStringLiteral("emblem-success")); } QIcon Formatting::infoIcon() { return QIcon::fromTheme(QStringLiteral("emblem-information")); } QIcon Formatting::questionIcon() { return QIcon::fromTheme(QStringLiteral("emblem-question")); } QIcon Formatting::unavailableIcon() { return QIcon::fromTheme(QStringLiteral("emblem-unavailable")); } QIcon Formatting::warningIcon() { return QIcon::fromTheme(QStringLiteral("emblem-warning")); } QIcon Formatting::errorIcon() { return QIcon::fromTheme(QStringLiteral("emblem-error")); } // // Name // QString Formatting::prettyName(int proto, const char *id, const char *name_, const char *comment_) { if (proto == GpgME::OpenPGP) { const QString name = QString::fromUtf8(name_); if (name.isEmpty()) { return QString(); } const QString comment = QString::fromUtf8(comment_); if (comment.isEmpty()) { return name; } return QStringLiteral("%1 (%2)").arg(name, comment); } if (proto == GpgME::CMS) { const DN subject(id); const QString cn = subject[QStringLiteral("CN")].trimmed(); if (cn.isEmpty()) { return subject.prettyDN(); } return cn; } return QString(); } QString Formatting::prettyNameAndEMail(int proto, const char *id, const char *name_, const char *email_, const char *comment_) { return prettyNameAndEMail(proto, QString::fromUtf8(id), QString::fromUtf8(name_), prettyEMail(email_, id), QString::fromUtf8(comment_)); } QString Formatting::prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment) { if (proto == GpgME::OpenPGP) { if (name.isEmpty()) { if (email.isEmpty()) { return QString(); } else if (comment.isEmpty()) { return QStringLiteral("<%1>").arg(email); } else { return QStringLiteral("(%2) <%1>").arg(email, comment); } } if (email.isEmpty()) { if (comment.isEmpty()) { return name; } else { return QStringLiteral("%1 (%2)").arg(name, comment); } } if (comment.isEmpty()) { return QStringLiteral("%1 <%2>").arg(name, email); } else { return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment); } } if (proto == GpgME::CMS) { const DN subject(id); const QString cn = subject[QStringLiteral("CN")].trimmed(); if (cn.isEmpty()) { return subject.prettyDN(); } return cn; } return QString(); } QString Formatting::prettyUserID(const UserID &uid) { if (uid.parent().protocol() == GpgME::OpenPGP) { return prettyNameAndEMail(uid); } const QByteArray id = QByteArray(uid.id()).trimmed(); if (id.startsWith('<')) { return prettyEMail(uid.email(), uid.id()); } if (id.startsWith('(')) { // ### parse uri/dns: return QString::fromUtf8(uid.id()); } else { return DN(uid.id()).prettyDN(); } } QString Formatting::prettyKeyID(const char *id) { if (!id) { return QString(); } return QLatin1String("0x") + QString::fromLatin1(id).toUpper(); } QString Formatting::prettyNameAndEMail(const UserID &uid) { return prettyNameAndEMail(uid.parent().protocol(), uid.id(), uid.name(), uid.email(), uid.comment()); } QString Formatting::prettyNameAndEMail(const Key &key) { return prettyNameAndEMail(key.userID(0)); } QString Formatting::prettyName(const Key &key) { return prettyName(key.userID(0)); } QString Formatting::prettyName(const UserID &uid) { return prettyName(uid.parent().protocol(), uid.id(), uid.name(), uid.comment()); } QString Formatting::prettyName(const UserID::Signature &sig) { return prettyName(GpgME::OpenPGP, sig.signerUserID(), sig.signerName(), sig.signerComment()); } // // EMail // QString Formatting::prettyEMail(const Key &key) { for (unsigned int i = 0, end = key.numUserIDs(); i < end; ++i) { const QString email = prettyEMail(key.userID(i)); if (!email.isEmpty()) { return email; } } return QString(); } QString Formatting::prettyEMail(const UserID &uid) { return prettyEMail(uid.email(), uid.id()); } QString Formatting::prettyEMail(const UserID::Signature &sig) { return prettyEMail(sig.signerEmail(), sig.signerUserID()); } QString Formatting::prettyEMail(const char *email_, const char *id) { QString email; QString name; QString comment; if (email_ && KEmailAddress::splitAddress(QString::fromUtf8(email_), name, email, comment) == KEmailAddress::AddressOk) { return email; } else { return DN(id)[QStringLiteral("EMAIL")].trimmed(); } } // // Tooltip // namespace { static QString protect_whitespace(QString s) { static const QLatin1Char SP(' '); static const QLatin1Char NBSP('\xA0'); return s.replace(SP, NBSP); } template QString format_row(const QString &field, const T_arg &arg) { return QStringLiteral("%1:%2").arg(protect_whitespace(field), arg); } QString format_row(const QString &field, const QString &arg) { return QStringLiteral("%1:%2").arg(protect_whitespace(field), arg.toHtmlEscaped()); } QString format_row(const QString &field, const char *arg) { return format_row(field, QString::fromUtf8(arg)); } QString format_keytype(const Key &key) { const Subkey subkey = key.subkey(0); if (key.hasSecret()) { return i18n("%1-bit %2 (secret key available)", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString())); } else { return i18n("%1-bit %2", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString())); } } QString format_subkeytype(const Subkey &subkey) { const auto algo = subkey.publicKeyAlgorithm(); if (algo == Subkey::AlgoECC || algo == Subkey::AlgoECDSA || algo == Subkey::AlgoECDH || algo == Subkey::AlgoEDDSA) { return QString::fromStdString(subkey.algoName()); } return i18n("%1-bit %2", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString())); } QString format_keyusage(const Key &key) { QStringList capabilities; if (Kleo::keyHasSign(key)) { if (key.isQualified()) { capabilities.push_back(i18n("Signing (Qualified)")); } else { capabilities.push_back(i18n("Signing")); } } if (Kleo::keyHasEncrypt(key)) { capabilities.push_back(i18n("Encryption")); } if (Kleo::keyHasCertify(key)) { capabilities.push_back(i18n("Certifying User-IDs")); } if (Kleo::keyHasAuthenticate(key)) { capabilities.push_back(i18n("SSH Authentication")); } return capabilities.join(QLatin1String(", ")); } QString format_subkeyusage(const Subkey &subkey) { QStringList capabilities; if (subkey.canSign()) { if (subkey.isQualified()) { capabilities.push_back(i18n("Signing (Qualified)")); } else { capabilities.push_back(i18n("Signing")); } } if (subkey.canEncrypt()) { capabilities.push_back(i18n("Encryption")); } if (subkey.canCertify()) { capabilities.push_back(i18n("Certifying User-IDs")); } if (subkey.canAuthenticate()) { capabilities.push_back(i18n("SSH Authentication")); } return capabilities.join(QLatin1String(", ")); } static QString time_t2string(time_t t) { const QDateTime dt = QDateTime::fromSecsSinceEpoch(quint32(t)); return QLocale().toString(dt, QLocale::ShortFormat); } static QString make_red(const QString &txt) { return QLatin1String("") + txt.toHtmlEscaped() + QLatin1String(""); } } QString Formatting::toolTip(const Key &key, int flags) { if (flags == 0 || (key.protocol() != GpgME::CMS && key.protocol() != GpgME::OpenPGP)) { return QString(); } const Subkey subkey = key.subkey(0); QString result; if (flags & Validity) { if (key.protocol() == GpgME::OpenPGP || (key.keyListMode() & Validate)) { if (key.isRevoked()) { result = make_red(i18n("Revoked")); } else if (key.isExpired()) { result = make_red(i18n("Expired")); } else if (key.isDisabled()) { result = i18n("Disabled"); } else if (key.keyListMode() & GpgME::Validate) { unsigned int fullyTrusted = 0; for (const auto &uid : key.userIDs()) { if (uid.validity() >= UserID::Validity::Full) { fullyTrusted++; } } if (fullyTrusted == key.numUserIDs()) { result = i18n("All User-IDs are certified."); const auto compliance = complianceStringForKey(key); if (!compliance.isEmpty()) { result += QStringLiteral("
") + compliance; } } else { result = i18np("One User-ID is not certified.", "%1 User-IDs are not certified.", key.numUserIDs() - fullyTrusted); } } else { result = i18n("The validity cannot be checked at the moment."); } } else { result = i18n("The validity cannot be checked at the moment."); } } if (flags == Validity) { return result; } result += QLatin1String(""); if (key.protocol() == GpgME::CMS) { if (flags & SerialNumber) { result += format_row(i18n("Serial number"), key.issuerSerial()); } if (flags & Issuer) { result += format_row(i18n("Issuer"), key.issuerName()); } } if (flags & UserIDs) { const std::vector uids = key.userIDs(); if (!uids.empty()) { result += format_row(key.protocol() == GpgME::CMS ? i18n("Subject") : i18n("User-ID"), prettyUserID(uids.front())); } if (uids.size() > 1) { for (auto it = uids.begin() + 1, end = uids.end(); it != end; ++it) { if (!it->isRevoked() && !it->isInvalid()) { result += format_row(i18n("a.k.a."), prettyUserID(*it)); } } } } if (flags & ExpiryDates) { result += format_row(i18n("Valid from"), time_t2string(subkey.creationTime())); if (!subkey.neverExpires()) { result += format_row(i18n("Valid until"), time_t2string(subkey.expirationTime())); } } if (flags & CertificateType) { result += format_row(i18n("Type"), format_keytype(key)); } if (flags & CertificateUsage) { result += format_row(i18n("Usage"), format_keyusage(key)); } if (flags & KeyID) { result += format_row(i18n("Key-ID"), QString::fromLatin1(key.shortKeyID())); } if (flags & Fingerprint) { result += format_row(i18n("Fingerprint"), key.primaryFingerprint()); } if (flags & OwnerTrust) { if (key.protocol() == GpgME::OpenPGP) { result += format_row(i18n("Certification trust"), ownerTrustShort(key)); } else if (key.isRoot()) { result += format_row(i18n("Trusted issuer?"), key.userID(0).validity() == UserID::Ultimate ? i18n("Yes") : i18n("No")); } } if (flags & StorageLocation) { if (const char *card = subkey.cardSerialNumber()) { result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card))); } else { result += format_row(i18n("Stored"), i18nc("stored...", "on this computer")); } } if (flags & Subkeys) { for (const auto &sub : key.subkeys()) { result += QLatin1String("
"); result += format_row(i18n("Subkey"), sub.fingerprint()); if (sub.isRevoked()) { result += format_row(i18n("Status"), i18n("Revoked")); } else if (sub.isExpired()) { result += format_row(i18n("Status"), i18n("Expired")); } if (flags & ExpiryDates) { result += format_row(i18n("Valid from"), time_t2string(sub.creationTime())); if (!sub.neverExpires()) { result += format_row(i18n("Valid until"), time_t2string(sub.expirationTime())); } } if (flags & CertificateType) { result += format_row(i18n("Type"), format_subkeytype(sub)); } if (flags & CertificateUsage) { result += format_row(i18n("Usage"), format_subkeyusage(sub)); } if (flags & StorageLocation) { if (const char *card = sub.cardSerialNumber()) { result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card))); } else { result += format_row(i18n("Stored"), i18nc("stored...", "on this computer")); } } } } result += QLatin1String("
"); return result; } namespace { template QString getValidityStatement(const Container &keys) { const bool allKeysAreOpenPGP = std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { return key.protocol() == GpgME::OpenPGP; }); const bool allKeysAreValidated = std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) { return key.keyListMode() & Validate; }); if (allKeysAreOpenPGP || allKeysAreValidated) { const bool someKeysAreBad = std::any_of(keys.cbegin(), keys.cend(), std::mem_fn(&Key::isBad)); if (someKeysAreBad) { return i18n("Some keys are revoked, expired, disabled, or invalid."); } else { const bool allKeysAreFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Kleo::allUserIDsHaveFullValidity); if (allKeysAreFullyValid) { return i18n("All keys are certified."); } else { return i18n("Some keys are not certified."); } } } return i18n("The validity of the keys cannot be checked at the moment."); } } QString Formatting::toolTip(const KeyGroup &group, int flags) { static const unsigned int maxNumKeysForTooltip = 20; if (group.isNull()) { return QString(); } const KeyGroup::Keys &keys = group.keys(); if (keys.size() == 0) { return i18nc("@info:tooltip", "This group does not contain any keys."); } const QString validity = (flags & Validity) ? getValidityStatement(keys) : QString(); if (flags == Validity) { return validity; } // list either up to maxNumKeysForTooltip keys or (maxNumKeysForTooltip-1) keys followed by "and n more keys" const unsigned int numKeysForTooltip = keys.size() > maxNumKeysForTooltip ? maxNumKeysForTooltip - 1 : keys.size(); QStringList result; result.reserve(3 + 2 + numKeysForTooltip + 2); if (!validity.isEmpty()) { result.push_back(QStringLiteral("

")); result.push_back(validity.toHtmlEscaped()); result.push_back(QStringLiteral("

")); } result.push_back(QStringLiteral("

")); result.push_back(i18n("Keys:")); { auto it = keys.cbegin(); for (unsigned int i = 0; i < numKeysForTooltip; ++i, ++it) { result.push_back(QLatin1String("
") + Formatting::summaryLine(*it).toHtmlEscaped()); } } if (keys.size() > numKeysForTooltip) { result.push_back(QLatin1String("
") + i18ncp("this follows a list of keys", "and 1 more key", "and %1 more keys", keys.size() - numKeysForTooltip)); } result.push_back(QStringLiteral("

")); return result.join(QLatin1Char('\n')); } // // Creation and Expiration // namespace { static QDate time_t2date(time_t t) { if (!t) { return {}; } const QDateTime dt = QDateTime::fromSecsSinceEpoch(quint32(t)); return dt.date(); } static QString accessible_date_format() { return i18nc( "date format suitable for screen readers; " "d: day as a number without a leading zero, " "MMMM: localized month name, " "yyyy: year as a four digit number", "MMMM d, yyyy"); } template QString expiration_date_string(const T &tee, const QString &noExpiration) { return tee.neverExpires() ? noExpiration : Formatting::dateString(time_t2date(tee.expirationTime())); } template QDate creation_date(const T &tee) { return time_t2date(tee.creationTime()); } template QDate expiration_date(const T &tee) { return time_t2date(tee.expirationTime()); } } QString Formatting::dateString(time_t t) { return dateString(time_t2date(t)); } QString Formatting::dateString(const QDate &date) { return QLocale().toString(date, QLocale::ShortFormat); } QString Formatting::accessibleDate(time_t t) { return accessibleDate(time_t2date(t)); } QString Formatting::accessibleDate(const QDate &date) { return QLocale().toString(date, accessible_date_format()); } QString Formatting::expirationDateString(const Key &key, const QString &noExpiration) { // if key is remote but has a non-zero expiration date (e.g. a key looked up via WKD), // then we assume that the date is valid; if the date is zero for a remote key, then // we don't know if it's unknown or unlimited return isRemoteKey(key) && (key.subkey(0).expirationTime() == 0) // ? i18nc("@info the expiration date of the key is unknown", "unknown") : expiration_date_string(key.subkey(0), noExpiration); } QString Formatting::expirationDateString(const Subkey &subkey, const QString &noExpiration) { return expiration_date_string(subkey, noExpiration); } QString Formatting::expirationDateString(const UserID::Signature &sig, const QString &noExpiration) { return expiration_date_string(sig, noExpiration); } QDate Formatting::expirationDate(const Key &key) { return expiration_date(key.subkey(0)); } QDate Formatting::expirationDate(const Subkey &subkey) { return expiration_date(subkey); } QDate Formatting::expirationDate(const UserID::Signature &sig) { return expiration_date(sig); } QString Formatting::accessibleExpirationDate(const Key &key, const QString &noExpiration) { // if key is remote but has a non-zero expiration date (e.g. a key looked up via WKD), // then we assume that the date is valid; if the date is zero for a remote key, then // we don't know if it's unknown or unlimited return isRemoteKey(key) && (key.subkey(0).expirationTime() == 0) // ? i18nc("@info the expiration date of the key is unknown", "unknown") : accessibleExpirationDate(key.subkey(0), noExpiration); } QString Formatting::accessibleExpirationDate(const Subkey &subkey, const QString &noExpiration) { if (subkey.neverExpires()) { return noExpiration.isEmpty() ? i18n("unlimited") : noExpiration; } else { return accessibleDate(expirationDate(subkey)); } } QString Formatting::accessibleExpirationDate(const UserID::Signature &sig, const QString &noExpiration) { if (sig.neverExpires()) { return noExpiration.isEmpty() ? i18n("unlimited") : noExpiration; } else { return accessibleDate(expirationDate(sig)); } } QString Formatting::creationDateString(const Key &key) { return dateString(creation_date(key.subkey(0))); } QString Formatting::creationDateString(const Subkey &subkey) { return dateString(creation_date(subkey)); } QString Formatting::creationDateString(const UserID::Signature &sig) { return dateString(creation_date(sig)); } QDate Formatting::creationDate(const Key &key) { return creation_date(key.subkey(0)); } QDate Formatting::creationDate(const Subkey &subkey) { return creation_date(subkey); } QDate Formatting::creationDate(const UserID::Signature &sig) { return creation_date(sig); } QString Formatting::accessibleCreationDate(const Key &key) { return accessibleDate(creationDate(key)); } QString Formatting::accessibleCreationDate(const Subkey &subkey) { return accessibleDate(creationDate(subkey)); } // // Types // QString Formatting::displayName(GpgME::Protocol p) { if (p == GpgME::CMS) { return i18nc("X.509/CMS encryption standard", "S/MIME"); } if (p == GpgME::OpenPGP) { return i18n("OpenPGP"); } return i18nc("Unknown encryption protocol", "Unknown"); } QString Formatting::type(const Key &key) { return displayName(key.protocol()); } QString Formatting::type(const Subkey &subkey) { return QString::fromUtf8(subkey.publicKeyAlgorithmAsString()); } QString Formatting::type(const KeyGroup &group) { Q_UNUSED(group) return i18nc("a group of keys/certificates", "Group"); } // // Status / Validity // QString Formatting::ownerTrustShort(const Key &key) { return ownerTrustShort(key.ownerTrust()); } QString Formatting::ownerTrustShort(Key::OwnerTrust trust) { switch (trust) { case Key::Unknown: return i18nc("unknown trust level", "unknown"); case Key::Never: return i18n("untrusted"); case Key::Marginal: return i18nc("marginal trust", "marginal"); case Key::Full: return i18nc("full trust", "full"); case Key::Ultimate: return i18nc("ultimate trust", "ultimate"); case Key::Undefined: return i18nc("undefined trust", "undefined"); default: Q_ASSERT(!"unexpected owner trust value"); break; } return QString(); } QString Formatting::validityShort(const Subkey &subkey) { if (subkey.isRevoked()) { return i18n("revoked"); } if (subkey.isExpired()) { return i18n("expired"); } if (subkey.isDisabled()) { return i18n("disabled"); } if (subkey.isInvalid()) { return i18n("invalid"); } return i18nc("as in good/valid signature", "good"); } QString Formatting::validityShort(const UserID &uid) { if (uid.isRevoked()) { return i18n("revoked"); } if (uid.isInvalid()) { return i18n("invalid"); } switch (uid.validity()) { case UserID::Unknown: return i18nc("unknown trust level", "unknown"); case UserID::Undefined: return i18nc("undefined trust", "undefined"); case UserID::Never: return i18n("untrusted"); case UserID::Marginal: return i18nc("marginal trust", "marginal"); case UserID::Full: return i18nc("full trust", "full"); case UserID::Ultimate: return i18nc("ultimate trust", "ultimate"); } return QString(); } QString Formatting::validityShort(const UserID::Signature &sig) { switch (sig.status()) { case UserID::Signature::NoError: if (!sig.isInvalid()) { /* See RFC 4880 Section 5.2.1 */ switch (sig.certClass()) { case 0x10: /* Generic */ case 0x11: /* Persona */ case 0x12: /* Casual */ case 0x13: /* Positive */ return i18n("valid"); case 0x30: return i18n("revoked"); default: return i18n("class %1", sig.certClass()); } } [[fallthrough]]; // fall through: case UserID::Signature::GeneralError: return i18n("invalid"); case UserID::Signature::SigExpired: return i18n("expired"); case UserID::Signature::KeyExpired: return i18n("certificate expired"); case UserID::Signature::BadSignature: return i18nc("fake/invalid signature", "bad"); case UserID::Signature::NoPublicKey: { /* GnuPG returns the same error for no public key as for expired * or revoked certificates. */ const auto key = KeyCache::instance()->findByKeyIDOrFingerprint(sig.signerKeyID()); if (key.isNull()) { return i18n("no public key"); } else if (key.isExpired()) { return i18n("key expired"); } else if (key.isRevoked()) { return i18n("key revoked"); } else if (key.isDisabled()) { return i18n("key disabled"); } /* can't happen */ return QStringLiteral("unknown"); } } return QString(); } QIcon Formatting::validityIcon(const UserID::Signature &sig) { switch (sig.status()) { case UserID::Signature::NoError: if (!sig.isInvalid()) { /* See RFC 4880 Section 5.2.1 */ switch (sig.certClass()) { case 0x10: /* Generic */ case 0x11: /* Persona */ case 0x12: /* Casual */ case 0x13: /* Positive */ return Formatting::successIcon(); case 0x30: return Formatting::errorIcon(); default: return QIcon(); } } [[fallthrough]]; // fall through: case UserID::Signature::BadSignature: case UserID::Signature::GeneralError: return Formatting::errorIcon(); case UserID::Signature::SigExpired: case UserID::Signature::KeyExpired: return Formatting::infoIcon(); case UserID::Signature::NoPublicKey: return Formatting::questionIcon(); } return QIcon(); } QString Formatting::formatKeyLink(const Key &key) { if (key.isNull()) { return QString(); } return QStringLiteral("%2").arg(QLatin1String(key.primaryFingerprint()), Formatting::prettyName(key)); } QString Formatting::formatForComboBox(const GpgME::Key &key) { const QString name = prettyName(key); QString mail = prettyEMail(key); if (!mail.isEmpty()) { mail = QLatin1Char('<') + mail + QLatin1Char('>'); } return i18nc("name, email, key id", "%1 %2 (%3)", name, mail, QLatin1String(key.shortKeyID())).simplified(); } +QString Formatting::nameAndEmailForSummaryLine(const UserID &id) +{ + Q_ASSERT(!id.isNull()); + + const QString email = Formatting::prettyEMail(id); + const QString name = Formatting::prettyName(id); + + if (name.isEmpty()) { + return email; + } else if (email.isEmpty()) { + return name; + } else { + return QStringLiteral("%1 <%2>").arg(name, email); + } +} + QString Formatting::nameAndEmailForSummaryLine(const Key &key) { Q_ASSERT(!key.isNull()); const QString email = Formatting::prettyEMail(key); const QString name = Formatting::prettyName(key); if (name.isEmpty()) { return email; } else if (email.isEmpty()) { return name; } else { return QStringLiteral("%1 <%2>").arg(name, email); } } const char *Formatting::summaryToString(const Signature::Summary summary) { if (summary & Signature::Red) { return "RED"; } if (summary & Signature::Green) { return "GREEN"; } return "YELLOW"; } QString Formatting::signatureToString(const Signature &sig, const Key &key) { if (sig.isNull()) { return QString(); } const bool red = (sig.summary() & Signature::Red); const bool valid = (sig.summary() & Signature::Valid); if (red) { if (key.isNull()) { if (const char *fpr = sig.fingerprint()) { return i18n("Bad signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status())); } else { return i18n("Bad signature by an unknown certificate: %1", Formatting::errorAsString(sig.status())); } } else { return i18n("Bad signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status())); } } else if (valid) { if (key.isNull()) { if (const char *fpr = sig.fingerprint()) { return i18n("Good signature by unknown certificate %1.", QString::fromLatin1(fpr)); } else { return i18n("Good signature by an unknown certificate."); } } else { return i18n("Good signature by %1.", nameAndEmailForSummaryLine(key)); } } else if (key.isNull()) { if (const char *fpr = sig.fingerprint()) { return i18n("Invalid signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status())); } else { return i18n("Invalid signature by an unknown certificate: %1", Formatting::errorAsString(sig.status())); } } else { return i18n("Invalid signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status())); } } // // ImportResult // QString Formatting::importMetaData(const Import &import, const QStringList &ids) { const QString result = importMetaData(import); if (result.isEmpty()) { return QString(); } else { return result + QLatin1Char('\n') + i18n("This certificate was imported from the following sources:") + QLatin1Char('\n') + ids.join(QLatin1Char('\n')); } } QString Formatting::importMetaData(const Import &import) { if (import.isNull()) { return QString(); } if (import.error().isCanceled()) { return i18n("The import of this certificate was canceled."); } if (import.error()) { return i18n("An error occurred importing this certificate: %1", Formatting::errorAsString(import.error())); } const unsigned int status = import.status(); if (status & Import::NewKey) { return (status & Import::ContainedSecretKey) ? i18n("This certificate was new to your keystore. The secret key is available.") : i18n("This certificate is new to your keystore."); } QStringList results; if (status & Import::NewUserIDs) { results.push_back(i18n("New user-ids were added to this certificate by the import.")); } if (status & Import::NewSignatures) { results.push_back(i18n("New signatures were added to this certificate by the import.")); } if (status & Import::NewSubkeys) { results.push_back(i18n("New subkeys were added to this certificate by the import.")); } return results.empty() ? i18n("The import contained no new data for this certificate. It is unchanged.") : results.join(QLatin1Char('\n')); } // // Overview in CertificateDetailsDialog // QString Formatting::formatOverview(const Key &key) { return toolTip(key, AllOptions); } QString Formatting::usageString(const Subkey &sub) { QStringList usageStrings; if (sub.canCertify()) { usageStrings << i18n("Certify"); } if (sub.canSign()) { usageStrings << i18n("Sign"); } if (sub.canEncrypt()) { usageStrings << i18n("Encrypt"); } if (sub.canAuthenticate()) { usageStrings << i18n("Authenticate"); } + if (sub.canRenc()) { + usageStrings << i18nc("Means 'Additional Decryption Subkey'; Don't try translating that, though.", "ADSK"); + } return usageStrings.join(QLatin1String(", ")); } +QString Formatting::summaryLine(const UserID &id) +{ + return i18nc("name (validity, protocol, creation date)", + "%1 (%2, %3, created: %4)", + nameAndEmailForSummaryLine(id), + Formatting::complianceStringShort(id), + displayName(id.parent().protocol()), + Formatting::creationDateString(id.parent())); +} + QString Formatting::summaryLine(const Key &key) { return nameAndEmailForSummaryLine(key) + QLatin1Char(' ') + i18nc("(validity, protocol, creation date)", "(%1, %2, created: %3)", Formatting::complianceStringShort(key), displayName(key.protocol()), Formatting::creationDateString(key)); } QString Formatting::summaryLine(const KeyGroup &group) { switch (group.source()) { case KeyGroup::ApplicationConfig: case KeyGroup::GnuPGConfig: return i18ncp("name of group of keys (n key(s), validity)", "%2 (1 key, %3)", "%2 (%1 keys, %3)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); case KeyGroup::Tags: return i18ncp("name of group of keys (n key(s), validity, tag)", "%2 (1 key, %3, tag)", "%2 (%1 keys, %3, tag)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); default: return i18ncp("name of group of keys (n key(s), validity, group ...)", "%2 (1 key, %3, unknown origin)", "%2 (%1 keys, %3, unknown origin)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); } } // Icon for certificate selection indication QIcon Formatting::iconForUid(const UserID &uid) { if (Kleo::isRevokedOrExpired(uid)) { return Formatting::errorIcon(); } return iconForValidity(uid); } QString Formatting::validity(const UserID &uid) { switch (uid.validity()) { case UserID::Ultimate: return i18n("The certificate is marked as your own."); case UserID::Full: return i18n("The certificate belongs to this recipient."); case UserID::Marginal: return i18n("The trust model indicates marginally that the certificate belongs to this recipient."); case UserID::Never: return i18n("This certificate should not be used."); case UserID::Undefined: case UserID::Unknown: default: return i18n("There is no indication that this certificate belongs to this recipient."); } } QString Formatting::validity(const KeyGroup &group) { if (group.isNull()) { return QString(); } const KeyGroup::Keys &keys = group.keys(); if (keys.size() == 0) { return i18n("This group does not contain any keys."); } return getValidityStatement(keys); } namespace { template UserID::Validity minimalValidity(const Container &keys) { const int minValidity = std::accumulate(keys.cbegin(), keys.cend(), UserID::Ultimate + 1, [](int validity, const Key &key) { return std::min(validity, minimalValidityOfNotRevokedUserIDs(key)); }); return minValidity <= UserID::Ultimate ? static_cast(minValidity) : UserID::Unknown; } template bool allKeysAreCompliant(const Container &keys) { if (!DeVSCompliance::isActive()) { return true; } if (!DeVSCompliance::isCompliant()) { return false; } return Kleo::all_of(keys, DeVSCompliance::keyIsCompliant); } } QIcon Formatting::validityIcon(const KeyGroup &group) { if (Kleo::any_of(group.keys(), std::mem_fn(&Key::isBad))) { return Formatting::errorIcon(); } return iconForValidityAndCompliance(minimalValidity(group.keys()), allKeysAreCompliant(group.keys())); } bool Formatting::uidsHaveFullValidity(const Key &key) { return allUserIDsHaveFullValidity(key); } QString Formatting::complianceMode() { const auto complianceValue = getCryptoConfigStringValue("gpg", "compliance"); return complianceValue == QLatin1String("gnupg") ? QString() : complianceValue; } bool Formatting::isKeyDeVs(const GpgME::Key &key) { return DeVSCompliance::allSubkeysAreCompliant(key); } QString Formatting::complianceStringForKey(const GpgME::Key &key) { // There will likely be more in the future for other institutions // for now we only have DE-VS if (DeVSCompliance::isCompliant()) { return isRemoteKey(key) // ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown") : DeVSCompliance::name(DeVSCompliance::keyIsCompliant(key)); } return QString(); } +QString Formatting::complianceStringShort(const GpgME::UserID &id) +{ + if (DeVSCompliance::isCompliant() && DeVSCompliance::userIDIsCompliant(id)) { + return QStringLiteral("★ ") + DeVSCompliance::name(true); + } + const bool keyValidityChecked = (id.parent().keyListMode() & GpgME::Validate); + if (keyValidityChecked && id.validity() >= UserID::Full) { + return i18nc("As in 'this user ID is valid.'", "certified"); + } + if (id.parent().isExpired() || isExpired(id)) { + return i18n("expired"); + } + if (id.parent().isRevoked() || id.isRevoked()) { + return i18n("revoked"); + } + if (id.parent().isDisabled()) { + return i18n("disabled"); + } + if (id.parent().isInvalid() || id.isInvalid()) { + return i18n("invalid"); + } + if (keyValidityChecked) { + return i18nc("As in 'this user ID is not certified'", "not certified"); + } + + return i18nc("The validity of this user ID has not been/could not be checked", "not checked"); +} + QString Formatting::complianceStringShort(const GpgME::Key &key) { if (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(key)) { return QStringLiteral("★ ") + DeVSCompliance::name(true); } const bool keyValidityChecked = (key.keyListMode() & GpgME::Validate); if (keyValidityChecked && Kleo::allUserIDsHaveFullValidity(key)) { return i18nc("As in all user IDs are valid.", "certified"); } if (key.isExpired()) { return i18n("expired"); } if (key.isRevoked()) { return i18n("revoked"); } if (key.isDisabled()) { return i18n("disabled"); } if (key.isInvalid()) { return i18n("invalid"); } if (keyValidityChecked) { return i18nc("As in not all user IDs are valid.", "not certified"); } return i18nc("The validity of the user IDs has not been/could not be checked", "not checked"); } QString Formatting::complianceStringShort(const KeyGroup &group) { const KeyGroup::Keys &keys = group.keys(); const bool allKeysFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Kleo::allUserIDsHaveFullValidity); if (allKeysFullyValid) { return i18nc("As in all keys are valid.", "all certified"); } return i18nc("As in not all keys are valid.", "not all certified"); } QString Formatting::prettyID(const char *id) { if (!id) { return QString(); } QString ret = QString::fromLatin1(id).toUpper().replace(QRegularExpression(QStringLiteral("(....)")), QStringLiteral("\\1 ")).trimmed(); // For the standard 10 group fingerprint let us use a double space in the // middle to increase readability if (ret.size() == 49) { ret.insert(24, QLatin1Char(' ')); } return ret; } QString Formatting::accessibleHexID(const char *id) { static const QRegularExpression groupOfFourRegExp{QStringLiteral("(?:(.)(.)(.)(.))")}; QString ret; ret = QString::fromLatin1(id); if (!ret.isEmpty() && (ret.size() % 4 == 0)) { ret = ret.replace(groupOfFourRegExp, QStringLiteral("\\1 \\2 \\3 \\4, ")).chopped(2); } return ret; } QString Formatting::origin(int o) { switch (o) { case Key::OriginKS: return i18n("Keyserver"); case Key::OriginDane: return QStringLiteral("DANE"); case Key::OriginWKD: return QStringLiteral("WKD"); case Key::OriginURL: return QStringLiteral("URL"); case Key::OriginFile: return i18n("File import"); case Key::OriginSelf: return i18n("Generated"); case Key::OriginOther: case Key::OriginUnknown: default: return i18n("Unknown"); } } QString Formatting::deVsString(bool compliant) { return DeVSCompliance::name(compliant); } namespace { QString formatTrustScope(const char *trustScope) { static const QRegularExpression escapedNonAlphaNum{QStringLiteral(R"(\\([^0-9A-Za-z]))")}; const auto scopeRegExp = QString::fromUtf8(trustScope); if (scopeRegExp.startsWith(u"<[^>]+[@.]") && scopeRegExp.endsWith(u">$")) { // looks like a trust scope regular expression created by gpg auto domain = scopeRegExp.mid(10, scopeRegExp.size() - 10 - 2); domain.replace(escapedNonAlphaNum, QStringLiteral(R"(\1)")); return domain; } return scopeRegExp; } } QString Formatting::trustSignatureDomain(const GpgME::UserID::Signature &sig) { return formatTrustScope(sig.trustScope()); } QString Formatting::trustSignature(const GpgME::UserID::Signature &sig) { switch (sig.trustValue()) { case TrustSignatureTrust::Partial: return i18nc("Certifies this key as partially trusted introducer for 'domain name'.", "Certifies this key as partially trusted introducer for '%1'.", trustSignatureDomain(sig)); case TrustSignatureTrust::Complete: return i18nc("Certifies this key as fully trusted introducer for 'domain name'.", "Certifies this key as fully trusted introducer for '%1'.", trustSignatureDomain(sig)); default: return {}; } } QString Formatting::errorAsString(const GpgME::Error &error) { #ifdef Q_OS_WIN // On Windows, we set GpgME resp. libgpg-error to return (translated) error messages as UTF-8 const char *s = error.asString(); qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1); qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s; qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray{s}.toPercentEncoding(); return QString::fromUtf8(s); #else return QString::fromLocal8Bit(error.asString()); #endif } + +QString Formatting::prettyAlgorithmName(const std::string &algorithm) +{ + static const std::map displayNames = { + {"brainpoolP256r1", i18nc("@info", "ECC (Brainpool P-256)")}, + {"brainpoolP384r1", i18nc("@info", "ECC (Brainpool P-384)")}, + {"brainpoolP512r1", i18nc("@info", "ECC (Brainpool P-512)")}, + {"curve25519", i18nc("@info", "ECC (Curve25519)")}, + {"curve448", i18nc("@info", "ECC (Curve448)")}, + {"nistp256", i18nc("@info", "ECC (NIST P-256)")}, + {"nistp384", i18nc("@info", "ECC (NIST P-384)")}, + {"nistp521", i18nc("@info", "ECC (NIST P-521)")}, + {"rsa2048", i18nc("@info", "RSA 2048")}, + {"rsa3072", i18nc("@info", "RSA 3072")}, + {"rsa4096", i18nc("@info", "RSA 4096")}, + }; + const auto it = displayNames.find(algorithm); + return (it != displayNames.end()) ? it->second : i18nc("@info", "Unknown algorithm"); +} diff --git a/src/utils/formatting.h b/src/utils/formatting.h index 4cfc2d542..e1299c28b 100644 --- a/src/utils/formatting.h +++ b/src/utils/formatting.h @@ -1,224 +1,232 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/formatting.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "keyusage.h" #include "kleo_export.h" #include #include class QString; class QDate; class QIcon; namespace GpgME { class Error; class Import; } namespace Kleo { class KeyGroup; namespace Formatting { class KLEO_EXPORT IconProvider { public: inline explicit IconProvider(KeyUsage::Flags requiredUsages) : usage{requiredUsages} { } QIcon icon(const GpgME::Key &key) const; QIcon icon(const KeyGroup &group) const; private: KeyUsage usage; }; KLEO_EXPORT QIcon successIcon(); KLEO_EXPORT QIcon infoIcon(); KLEO_EXPORT QIcon questionIcon(); KLEO_EXPORT QIcon unavailableIcon(); KLEO_EXPORT QIcon warningIcon(); KLEO_EXPORT QIcon errorIcon(); KLEO_EXPORT QString prettyNameAndEMail(int proto, const char *id, const char *name, const char *email, const char *comment = nullptr); KLEO_EXPORT QString prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment = {}); KLEO_EXPORT QString prettyNameAndEMail(const GpgME::Key &key); KLEO_EXPORT QString prettyNameAndEMail(const GpgME::UserID &key); KLEO_EXPORT QString prettyUserID(const GpgME::UserID &uid); KLEO_EXPORT QString prettyKeyID(const char *id); KLEO_EXPORT QString prettyName(int proto, const char *id, const char *name, const char *comment = nullptr); KLEO_EXPORT QString prettyName(const GpgME::Key &key); KLEO_EXPORT QString prettyName(const GpgME::UserID &uid); KLEO_EXPORT QString prettyName(const GpgME::UserID::Signature &sig); KLEO_EXPORT QString prettyEMail(const char *email, const char *id); KLEO_EXPORT QString prettyEMail(const GpgME::Key &key); KLEO_EXPORT QString prettyEMail(const GpgME::UserID &uid); KLEO_EXPORT QString prettyEMail(const GpgME::UserID::Signature &sig); /* Formats a fingerprint or keyid into groups of four */ KLEO_EXPORT QString prettyID(const char *id); KLEO_EXPORT QString accessibleHexID(const char *id); // clang-format off enum ToolTipOption { KeyID = 0x001, Validity = 0x002, StorageLocation = 0x004, SerialNumber = 0x008, Issuer = 0x010, Subject = 0x020, ExpiryDates = 0x040, CertificateType = 0x080, CertificateUsage = 0x100, Fingerprint = 0x200, UserIDs = 0x400, OwnerTrust = 0x800, Subkeys = 0x1000, AllOptions = 0xffff }; // clang-format on KLEO_EXPORT QString toolTip(const GpgME::Key &key, int opts); KLEO_EXPORT QString toolTip(const Kleo::KeyGroup &group, int opts); /// Returns expiration date of @p key as string, or @p noExpiration if the key doesn't expire. KLEO_EXPORT QString expirationDateString(const GpgME::Key &key, const QString &noExpiration = {}); /// Returns expiration date of @p subkey as string, or @p noExpiration if the subkey doesn't expire. KLEO_EXPORT QString expirationDateString(const GpgME::Subkey &subkey, const QString &noExpiration = {}); /// Returns expiration date of @p sig as string, or @p noExpiration if the signature doesn't expire. KLEO_EXPORT QString expirationDateString(const GpgME::UserID::Signature &sig, const QString &noExpiration = {}); KLEO_EXPORT QDate expirationDate(const GpgME::Key &key); KLEO_EXPORT QDate expirationDate(const GpgME::Subkey &subkey); KLEO_EXPORT QDate expirationDate(const GpgME::UserID::Signature &sig); /** * Returns expiration date of @p key as string suitable for screen readers. * If the key doesn't expire, then it returns @p noExpiration if @p noExpiration is not empty. Otherwise, * returns the localization of "unlimited". */ KLEO_EXPORT QString accessibleExpirationDate(const GpgME::Key &key, const QString &noExpiration = {}); /** * Returns expiration date of @p subkey as string suitable for screen readers. * If the subkey doesn't expire, then it returns @p noExpiration if @p noExpiration is not empty. Otherwise, * returns the localization of "unlimited". */ KLEO_EXPORT QString accessibleExpirationDate(const GpgME::Subkey &subkey, const QString &noExpiration = {}); /** * Returns expiration date of @p sig as string suitable for screen readers. * If the signature doesn't expire, then it returns @p noExpiration if @p noExpiration is not empty. Otherwise, * returns the localization of "unlimited". */ KLEO_EXPORT QString accessibleExpirationDate(const GpgME::UserID::Signature &sig, const QString &noExpiration = {}); KLEO_EXPORT QString creationDateString(const GpgME::Key &key); KLEO_EXPORT QString creationDateString(const GpgME::Subkey &subkey); KLEO_EXPORT QString creationDateString(const GpgME::UserID::Signature &sig); KLEO_EXPORT QDate creationDate(const GpgME::Key &key); KLEO_EXPORT QDate creationDate(const GpgME::Subkey &subkey); KLEO_EXPORT QDate creationDate(const GpgME::UserID::Signature &sig); KLEO_EXPORT QString accessibleCreationDate(const GpgME::Key &key); KLEO_EXPORT QString accessibleCreationDate(const GpgME::Subkey &subkey); /* Convert a GPGME style time or a QDate to a localized string */ KLEO_EXPORT QString dateString(time_t t); KLEO_EXPORT QString dateString(const QDate &date); KLEO_EXPORT QString accessibleDate(time_t t); KLEO_EXPORT QString accessibleDate(const QDate &date); KLEO_EXPORT QString displayName(GpgME::Protocol prot); KLEO_EXPORT QString type(const GpgME::Key &key); KLEO_EXPORT QString type(const GpgME::Subkey &subkey); KLEO_EXPORT QString type(const Kleo::KeyGroup &group); KLEO_EXPORT QString ownerTrustShort(const GpgME::Key &key); KLEO_EXPORT QString ownerTrustShort(GpgME::Key::OwnerTrust trust); KLEO_EXPORT QString validityShort(const GpgME::Subkey &subkey); KLEO_EXPORT QString validityShort(const GpgME::UserID &uid); KLEO_EXPORT QString validityShort(const GpgME::UserID::Signature &sig); KLEO_EXPORT QIcon validityIcon(const GpgME::UserID::Signature &sig); /* A sentence about the validity of the UserID */ KLEO_EXPORT QString validity(const GpgME::UserID &uid); KLEO_EXPORT QString validity(const Kleo::KeyGroup &group); KLEO_EXPORT QIcon validityIcon(const Kleo::KeyGroup &group); KLEO_EXPORT QString formatForComboBox(const GpgME::Key &key); KLEO_EXPORT QString formatKeyLink(const GpgME::Key &key); KLEO_EXPORT QString signatureToString(const GpgME::Signature &sig, const GpgME::Key &key); KLEO_EXPORT const char *summaryToString(const GpgME::Signature::Summary summary); KLEO_EXPORT QString importMetaData(const GpgME::Import &import); KLEO_EXPORT QString importMetaData(const GpgME::Import &import, const QStringList &sources); KLEO_EXPORT QString formatOverview(const GpgME::Key &key); KLEO_EXPORT QString usageString(const GpgME::Subkey &subkey); +KLEO_EXPORT QString summaryLine(const GpgME::UserID &id); KLEO_EXPORT QString summaryLine(const GpgME::Key &key); KLEO_EXPORT QString summaryLine(const KeyGroup &group); KLEO_EXPORT QString nameAndEmailForSummaryLine(const GpgME::Key &key); +KLEO_EXPORT QString nameAndEmailForSummaryLine(const GpgME::UserID &id); KLEO_EXPORT QIcon iconForUid(const GpgME::UserID &uid); /* Is the key valid i.e. are all uids fully trusted? */ KLEO_EXPORT bool uidsHaveFullValidity(const GpgME::Key &key); /* The compliance mode of the gnupg system. Empty if compliance * mode is not set. * Use Kleo::gnupgComplianceMode() instead. */ KLEO_DEPRECATED_EXPORT QString complianceMode(); /* Is the given key in compliance with CO_DE_VS? */ KLEO_EXPORT bool isKeyDeVs(const GpgME::Key &key); /** * Use Kleo::DeVSCompliance::name(bool) instead. */ KLEO_DEPRECATED_EXPORT QString deVsString(bool compliant = true); /* A sentence if the key confirms to the current compliance mode */ KLEO_EXPORT QString complianceStringForKey(const GpgME::Key &key); /* A single word for use in keylists to describe the validity of the * given key, including any conformance statements relevant to the * current conformance mode. */ KLEO_EXPORT QString complianceStringShort(const GpgME::Key &key); +KLEO_EXPORT QString complianceStringShort(const GpgME::UserID &id); KLEO_EXPORT QString complianceStringShort(const Kleo::KeyGroup &group); /* The origin of the key mapped to a localized string */ KLEO_EXPORT QString origin(int o); /* Human-readable trust signature scope (for trust signature regexp created by GnuPG) */ KLEO_EXPORT QString trustSignatureDomain(const GpgME::UserID::Signature &sig); /* Summary of trust signature properties */ KLEO_EXPORT QString trustSignature(const GpgME::UserID::Signature &sig); /** * Returns the value of Error::asString() for the error \p error as Unicode string. */ KLEO_EXPORT QString errorAsString(const GpgME::Error &error); + +/** + * Returns a name suitable for being displayed for the GPG algorithm name @p algorithm. + */ +KLEO_EXPORT QString prettyAlgorithmName(const std::string &algorithm); } } diff --git a/src/utils/gnupg.cpp b/src/utils/gnupg.cpp index 0aa31c03b..fa2ad6893 100644 --- a/src/utils/gnupg.cpp +++ b/src/utils/gnupg.cpp @@ -1,661 +1,669 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/gnupg.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2020-2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "gnupg.h" #include "assuan.h" #include "compat.h" #include "compliance.h" #include "cryptoconfig.h" #include "hex.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include "gnupg-registry.h" #endif // Q_OS_WIN #include #include using namespace GpgME; QString Kleo::gnupgHomeDirectory() { static const QString homeDir = QString::fromUtf8(GpgME::dirInfo("homedir")); return homeDir; } QString Kleo::gnupgPrivateKeysDirectory() { static const QString dir = QDir{gnupgHomeDirectory()}.filePath(QStringLiteral("private-keys-v1.d")); return dir; } int Kleo::makeGnuPGError(int code) { return gpg_error(static_cast(code)); } static QString findGpgExe(GpgME::Engine engine, const QString &exe) { const GpgME::EngineInfo info = GpgME::engineInfo(engine); return info.fileName() ? QFile::decodeName(info.fileName()) : QStandardPaths::findExecutable(exe); } QString Kleo::gpgConfPath() { static const auto path = findGpgExe(GpgME::GpgConfEngine, QStringLiteral("gpgconf")); return path; } QString Kleo::gpgSmPath() { static const auto path = findGpgExe(GpgME::GpgSMEngine, QStringLiteral("gpgsm")); return path; } QString Kleo::gpgPath() { static const auto path = findGpgExe(GpgME::GpgEngine, QStringLiteral("gpg")); return path; } QStringList Kleo::gnupgFileWhitelist() { return { // The obvious pubring QStringLiteral("pubring.gpg"), // GnuPG 2.1 pubring QStringLiteral("pubring.kbx"), // Trust in X509 Certificates QStringLiteral("trustlist.txt"), // Trustdb controls ownertrust and thus WOT validity QStringLiteral("trustdb.gpg"), // We want to update when smartcard status changes QStringLiteral("reader*.status"), // No longer used in 2.1 but for 2.0 we want this QStringLiteral("secring.gpg"), // Secret keys (living under private-keys-v1.d/) QStringLiteral("*.key"), // Changes to the trustmodel / compliance mode might // affect validity so we check this, too. // Globbing for gpg.conf* here will trigger too often // as gpgconf creates files like gpg.conf.bak or // gpg.conf.tmp12312.gpgconf that should not trigger // a change. QStringLiteral("gpg.conf"), QStringLiteral("gpg.conf-?"), QStringLiteral("gpg.conf-?.?"), }; } QStringList Kleo::gnupgFolderWhitelist() { static const QDir gnupgHome{gnupgHomeDirectory()}; return { gnupgHome.path(), gnupgPrivateKeysDirectory(), }; } QString Kleo::gpg4winInstallPath() { #ifdef Q_OS_WIN // QApplication::applicationDirPath is only used as a fallback // to support the case where Kleopatra is not installed from // Gpg4win but Gpg4win is also installed. char *instDir = read_w32_registry_string("HKEY_LOCAL_MACHINE", "Software\\GPG4Win", "Install Directory"); if (!instDir) { // Fallback to HKCU instDir = read_w32_registry_string("HKEY_CURRENT_USER", "Software\\GPG4Win", "Install Directory"); } if (instDir) { QString ret = QString::fromLocal8Bit(instDir) + QStringLiteral("/bin"); free(instDir); return ret; } qCDebug(LIBKLEO_LOG) << "Gpg4win not found. Falling back to Kleopatra instdir."; #endif return QCoreApplication::applicationDirPath(); } QString Kleo::gnupgInstallPath() { #ifdef Q_OS_WIN // QApplication::applicationDirPath is only used as a fallback // to support the case where Kleopatra is not installed from // Gpg4win but Gpg4win is also installed. char *instDir = read_w32_registry_string("HKEY_LOCAL_MACHINE", "Software\\GnuPG", "Install Directory"); if (!instDir) { // Fallback to HKCU instDir = read_w32_registry_string("HKEY_CURRENT_USER", "Software\\GnuPG", "Install Directory"); } if (instDir) { QString ret = QString::fromLocal8Bit(instDir) + QStringLiteral("/bin"); free(instDir); return ret; } qCDebug(LIBKLEO_LOG) << "GnuPG not found. Falling back to gpgconf list dir."; #endif return gpgConfListDir("bindir"); } QString Kleo::gpgConfListDir(const char *which) { if (!which || !*which) { return QString(); } const QString gpgConfPath = Kleo::gpgConfPath(); if (gpgConfPath.isEmpty()) { return QString(); } QProcess gpgConf; qCDebug(LIBKLEO_LOG) << "gpgConfListDir: starting " << qPrintable(gpgConfPath) << " --list-dirs"; gpgConf.start(gpgConfPath, QStringList() << QStringLiteral("--list-dirs")); if (!gpgConf.waitForFinished()) { qCDebug(LIBKLEO_LOG) << "gpgConfListDir(): failed to execute gpgconf: " << qPrintable(gpgConf.errorString()); qCDebug(LIBKLEO_LOG) << "output was:\n" << gpgConf.readAllStandardError().constData(); return QString(); } const QList lines = gpgConf.readAllStandardOutput().split('\n'); for (const QByteArray &line : lines) { if (line.startsWith(which) && line[qstrlen(which)] == ':') { const int begin = qstrlen(which) + 1; int end = line.size(); while (end && (line[end - 1] == '\n' || line[end - 1] == '\r')) { --end; } const QString result = QDir::fromNativeSeparators(QFile::decodeName(hexdecode(line.mid(begin, end - begin)))); qCDebug(LIBKLEO_LOG) << "gpgConfListDir: found " << qPrintable(result) << " for '" << which << "'entry"; return result; } } qCDebug(LIBKLEO_LOG) << "gpgConfListDir(): didn't find '" << which << "'" << "entry in output:\n" << gpgConf.readAllStandardError().constData(); return QString(); } static std::array getVersionFromString(const char *actual, bool &ok) { std::array ret; ok = false; if (!actual) { return ret; } QString versionString = QString::fromLatin1(actual); // Try to fix it up QRegularExpression rx(QRegularExpression::anchoredPattern(QLatin1String(R"((\d+)\.(\d+)\.(\d+)(?:-svn\d+)?.*)"))); QRegularExpressionMatch match; for (int i = 0; i < 3; i++) { match = rx.match(versionString); if (!match.hasMatch()) { versionString += QStringLiteral(".0"); } else { ok = true; break; } } if (!ok) { qCDebug(LIBKLEO_LOG) << "Can't parse version " << actual; return ret; } for (int i = 0; i < 3; ++i) { ret[i] = match.capturedView(i + 1).toUInt(&ok); if (!ok) { return ret; } } ok = true; return ret; } bool Kleo::versionIsAtLeast(const char *minimum, const char *actual) { if (!minimum || !actual) { return false; } bool ok; const auto minimum_version = getVersionFromString(minimum, ok); if (!ok) { return false; } const auto actual_version = getVersionFromString(actual, ok); if (!ok) { return false; } return !std::lexicographical_compare(std::begin(actual_version), std::end(actual_version), std::begin(minimum_version), std::end(minimum_version)); } bool Kleo::engineIsVersion(int major, int minor, int patch, GpgME::Engine engine) { static QMap> cachedVersions; const int required_version[] = {major, minor, patch}; // Gpgconf means spawning processes which is expensive on windows. std::array actual_version; if (!cachedVersions.contains(engine)) { const Error err = checkEngine(engine); if (err.code() == GPG_ERR_INV_ENGINE) { qCDebug(LIBKLEO_LOG) << "isVersion: invalid engine. '"; return false; } const char *actual = GpgME::engineInfo(engine).version(); bool ok; actual_version = getVersionFromString(actual, ok); qCDebug(LIBKLEO_LOG) << "Parsed" << actual << "as: " << actual_version[0] << '.' << actual_version[1] << '.' << actual_version[2] << '.'; if (!ok) { return false; } cachedVersions.insert(engine, actual_version); } else { actual_version = cachedVersions.value(engine); } // return ! ( actual_version < required_version ) return !std::lexicographical_compare(std::begin(actual_version), std::end(actual_version), std::begin(required_version), std::end(required_version)); } const QString &Kleo::paperKeyInstallPath() { static const QString pkPath = (QStandardPaths::findExecutable(QStringLiteral("paperkey"), QStringList() << QCoreApplication::applicationDirPath()).isEmpty() ? QStandardPaths::findExecutable(QStringLiteral("paperkey")) : QStandardPaths::findExecutable(QStringLiteral("paperkey"), QStringList() << QCoreApplication::applicationDirPath())); return pkPath; } bool Kleo::haveKeyserverConfigured() { + if (engineIsVersion(2, 4, 4) // + || (engineIsVersion(2, 2, 42) && !engineIsVersion(2, 3, 0))) { + return Kleo::keyserver() != QLatin1String{"none"}; + } if (engineIsVersion(2, 1, 19)) { // since 2.1.19 there is a builtin keyserver return true; } return !Kleo::keyserver().isEmpty(); } QString Kleo::keyserver() { QString result = getCryptoConfigStringValue("gpg", "keyserver"); if (result.isEmpty()) { result = getCryptoConfigStringValue("dirmngr", "keyserver"); } + if (result.endsWith(QLatin1String{"://none"})) { + // map hkps://none, etc., to "none"; see https://dev.gnupg.org/T6708 + result = QStringLiteral("none"); + } return result; } bool Kleo::haveX509DirectoryServerConfigured() { return !getCryptoConfigUrlList("dirmngr", "ldapserver").empty() // || !getCryptoConfigUrlList("dirmngr", "LDAP Server").empty() // || !getCryptoConfigUrlList("gpgsm", "keyserver").empty(); } bool Kleo::gpgComplianceP(const char *mode) { const auto conf = QGpgME::cryptoConfig(); const auto entry = getCryptoConfigEntry(conf, "gpg", "compliance"); return entry && entry->stringValue() == QString::fromLatin1(mode); } bool Kleo::gnupgUsesDeVsCompliance() { return DeVSCompliance::isActive(); } bool Kleo::gnupgIsDeVsCompliant() { return DeVSCompliance::isCompliant(); } #ifdef Q_OS_WIN static unsigned int guessConsoleOutputCodePage() { /* Qt on Windows uses GetACP while GnuPG prefers * GetConsoleOutputCP. * * As we are not a console application GetConsoleOutputCP * usually returns 0. * From experience the closest thing that let's us guess * what GetConsoleOutputCP returns for a console application * it appears to be the OEMCP. */ unsigned int cpno = GetConsoleOutputCP(); if (!cpno) { cpno = GetOEMCP(); } if (!cpno) { cpno = GetACP(); } if (!cpno) { qCDebug(LIBKLEO_LOG) << __func__ << "Failed to find native codepage"; } qCDebug(LIBKLEO_LOG) << __func__ << "returns" << cpno; return cpno; } static QString fromEncoding(unsigned int src_encoding, const char *data) { if (!data || !*data) { return {}; } // returns necessary buffer size including the terminating null character int n = MultiByteToWideChar(src_encoding, 0, data, -1, NULL, 0); if (n <= 0) { qCDebug(LIBKLEO_LOG) << __func__ << "determining necessary buffer size failed with error code" << GetLastError(); return QString(); } wchar_t *result = (wchar_t *)malloc((n + 1) * sizeof *result); n = MultiByteToWideChar(src_encoding, 0, data, -1, result, n); if (n <= 0) { free(result); qCDebug(LIBKLEO_LOG) << __func__ << "conversion failed with error code" << GetLastError(); return QString(); } const auto ret = QString::fromWCharArray(result, n - 1); free(result); return ret; } static QString stringFromGpgOutput_legacy(const QByteArray &ba) { static const unsigned int cpno = guessConsoleOutputCodePage(); if (cpno) { qCDebug(LIBKLEO_LOG) << __func__ << "trying to decode" << ba << "using codepage" << cpno; const auto rawData = QByteArray{ba}.replace("\r\n", "\n"); const auto s = fromEncoding(cpno, rawData.constData()); if (!s.isEmpty() || ba.isEmpty()) { return s; } qCDebug(LIBKLEO_LOG) << __func__ << "decoding output failed; falling back to QString::fromLocal8Bit()"; } qCDebug(LIBKLEO_LOG) << __func__ << "decoding from local encoding:" << ba; return QString::fromLocal8Bit(ba); } #endif QString Kleo::stringFromGpgOutput(const QByteArray &ba) { #ifdef Q_OS_WIN // since 2.2.28, GnuPG always uses UTF-8 for console output (and input) if (Kleo::engineIsVersion(2, 2, 28, GpgME::GpgEngine)) { return QString::fromUtf8(ba); } else { return stringFromGpgOutput_legacy(ba); } #else return QString::fromLocal8Bit(ba); #endif } QStringList Kleo::backendVersionInfo() { QStringList versions; if (Kleo::engineIsVersion(2, 2, 24, GpgME::GpgConfEngine)) { QProcess p; qCDebug(LIBKLEO_LOG) << "Running gpgconf --show-versions ..."; p.start(Kleo::gpgConfPath(), {QStringLiteral("--show-versions")}); // wait at most 1 second if (!p.waitForFinished(1000)) { qCDebug(LIBKLEO_LOG) << "Running gpgconf --show-versions timed out after 1 second."; } else if (p.exitStatus() != QProcess::NormalExit || p.exitCode() != 0) { qCDebug(LIBKLEO_LOG) << "Running gpgconf --show-versions failed:" << p.errorString(); qCDebug(LIBKLEO_LOG) << "gpgconf stderr:" << p.readAllStandardError(); qCDebug(LIBKLEO_LOG) << "gpgconf stdout:" << p.readAllStandardOutput(); } else { const QByteArray output = p.readAllStandardOutput().replace("\r\n", "\n"); - qCDebug(LIBKLEO_LOG) << "gpgconf stdout:" << p.readAllStandardOutput(); + qCDebug(LIBKLEO_LOG) << "gpgconf stdout:" << output; const auto lines = output.split('\n'); for (const auto &line : lines) { if (line.startsWith("* GnuPG") || line.startsWith("* Libgcrypt")) { const auto components = line.split(' '); versions.push_back(QString::fromLatin1(components.at(1) + ' ' + components.value(2))); } } } } return versions; } namespace { template auto startGpgConf(const QStringList &arguments, Function1 onSuccess, Function2 onFailure) { auto process = new QProcess; process->setProgram(Kleo::gpgConfPath()); process->setArguments(arguments); QObject::connect(process, &QProcess::started, [process]() { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") was started successfully"; }); QObject::connect(process, &QProcess::errorOccurred, [process, onFailure](auto error) { qCDebug(LIBKLEO_LOG).nospace() << "Error while running gpgconf (" << process << "): " << error; process->deleteLater(); onFailure(); }); QObject::connect(process, &QProcess::readyReadStandardError, [process]() { for (const auto &line : process->readAllStandardError().trimmed().split('\n')) { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") stderr: " << line; } }); QObject::connect(process, &QProcess::readyReadStandardOutput, [process]() { (void)process->readAllStandardOutput(); /* ignore stdout */ }); QObject::connect(process, qOverload(&QProcess::finished), [process, onSuccess, onFailure](int exitCode, QProcess::ExitStatus exitStatus) { if (exitStatus == QProcess::NormalExit) { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") exited (exit code: " << exitCode << ")"; if (exitCode == 0) { onSuccess(); } else { onFailure(); } } else { qCDebug(LIBKLEO_LOG).nospace() << "gpgconf (" << process << ") crashed (exit code: " << exitCode << ")"; onFailure(); } process->deleteLater(); }); qCDebug(LIBKLEO_LOG).nospace() << "Starting gpgconf (" << process << ") with arguments " << process->arguments().join(QLatin1Char(' ')) << " ..."; process->start(); return process; } static auto startGpgConf(const QStringList &arguments) { return startGpgConf( arguments, []() {}, []() {}); } } void Kleo::launchGpgAgent() { static QPointer process; static qint64 mSecsSinceEpochOfLastLaunch = 0; static int numberOfFailedLaunches = 0; if (Kleo::Assuan::agentIsRunning()) { qCDebug(LIBKLEO_LOG) << __func__ << ": gpg-agent is already running"; return; } if (process) { qCDebug(LIBKLEO_LOG) << __func__ << ": gpg-agent is already being launched"; return; } const auto now = QDateTime::currentMSecsSinceEpoch(); if (now - mSecsSinceEpochOfLastLaunch < 1000) { // reduce attempts to launch the agent to 1 attempt per second return; } mSecsSinceEpochOfLastLaunch = now; if (numberOfFailedLaunches > 5) { qCWarning(LIBKLEO_LOG) << __func__ << ": Launching gpg-agent failed" << numberOfFailedLaunches << "times in a row. Giving up."; return; } process = startGpgConf( {QStringLiteral("--launch"), QStringLiteral("gpg-agent")}, []() { numberOfFailedLaunches = 0; }, []() { numberOfFailedLaunches++; }); } void Kleo::killDaemons() { static QPointer process; if (process) { qCDebug(LIBKLEO_LOG) << __func__ << ": The daemons are already being shut down"; return; } process = startGpgConf({QStringLiteral("--kill"), QStringLiteral("all")}); } const std::vector &Kleo::availableAlgorithms() { static const std::vector algos = { "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", "curve25519", "curve448", "nistp256", "nistp384", "nistp521", "rsa2048", "rsa3072", "rsa4096", // "secp256k1", // Curve secp256k1 is explicitly ignored }; return algos; } const std::vector &Kleo::preferredAlgorithms() { static const std::vector algos = { "curve25519", "brainpoolP256r1", "rsa3072", "rsa2048", }; return algos; } const std::vector &Kleo::ignoredAlgorithms() { static const std::vector algos = { "secp256k1", // Curve secp256k1 is not useful }; return algos; } bool Kleo::gpgvVerify(const QString &filePath, const QString &sigPath, const QString &keyring, const QStringList &additionalSearchPaths) { const QFileInfo verifyFi(filePath); if (!verifyFi.isReadable()) { return false; } else { qCDebug(LIBKLEO_LOG) << "Verifying" << filePath; } const auto gpgvPath = QStandardPaths::findExecutable(QStringLiteral("gpgv"), additionalSearchPaths); if (gpgvPath.isEmpty()) { qCDebug(LIBKLEO_LOG) << "Could not find gpgv"; return false; } QFileInfo sigFi; if (!sigPath.isEmpty()) { sigFi.setFile(sigPath); } else { sigFi.setFile(filePath + QStringLiteral(".sig")); } if (!sigFi.isReadable()) { qCDebug(LIBKLEO_LOG) << "No signature found at" << sigFi.absoluteFilePath(); return false; } auto process = QProcess(); process.setProgram(gpgvPath); QStringList args; if (!keyring.isEmpty()) { args << QStringLiteral("--keyring") << keyring; } args << QStringLiteral("--") << sigFi.absoluteFilePath() << verifyFi.absoluteFilePath(); process.setArguments(args); qCDebug(LIBKLEO_LOG).nospace() << "Starting gpgv (" << gpgvPath << ") with arguments " << args.join(QLatin1Char(' ')) << " ..."; process.start(); if (!process.waitForFinished(-1)) { qCDebug(LIBKLEO_LOG) << "Failed to execute gpgv" << process.errorString(); } bool ret = (process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0); if (!ret) { qCDebug(LIBKLEO_LOG) << "Failed to verify file"; qCDebug(LIBKLEO_LOG) << "gpgv stdout:" << QString::fromUtf8(process.readAllStandardOutput()); qCDebug(LIBKLEO_LOG) << "gpgv stderr:" << QString::fromUtf8(process.readAllStandardError()); } return ret; } diff --git a/src/utils/gnupg.h b/src/utils/gnupg.h index 3a4eff882..2919bb988 100644 --- a/src/utils/gnupg.h +++ b/src/utils/gnupg.h @@ -1,138 +1,146 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/gnupg.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020-2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include #include class QString; class QByteArray; namespace Kleo { KLEO_EXPORT QString gnupgHomeDirectory(); KLEO_EXPORT QString gnupgPrivateKeysDirectory(); KLEO_EXPORT QString gpgConfPath(); KLEO_EXPORT QString gpgSmPath(); KLEO_EXPORT QString gpgPath(); KLEO_EXPORT QString gpgConfListDir(const char *which); KLEO_EXPORT QString gpg4winInstallPath(); KLEO_EXPORT QString gnupgInstallPath(); KLEO_EXPORT const QString &paperKeyInstallPath(); /** * Verify \p filePath using gpgv. If \p sigPath is provided it uses * this signature, otherwise it adds .sig to the \p filePath. If * \p keyring is provided that is the keyring where the signature is * checked against. Otherwise it uses the default of gpgv. * \p additionalSearchPaths can be used to specify where gpgv is * searched for first. * * Blocks until the verification is done which can be indefinetly to * allow for very large files. * * Returns true if the verification was successful, false if any problem * occured. */ KLEO_EXPORT bool gpgvVerify(const QString &filePath, const QString &sigPath = {}, const QString &keyring = {}, const QStringList &additionalSearchPaths = {}); /** * Returns a list of filename globs of files in one of the whitelisted folders * to watch for changes. * \sa gnupgFolderWhitelist, Kleo::FileSystemWatcher */ KLEO_EXPORT QStringList gnupgFileWhitelist(); /** * Returns a list of absolute paths of folders to watch for changes. * \sa gnupgFileWhitelist, Kleo::FileSystemWatcher */ KLEO_EXPORT QStringList gnupgFolderWhitelist(); KLEO_EXPORT int makeGnuPGError(int code); KLEO_EXPORT bool engineIsVersion(int major, int minor, int patch, GpgME::Engine = GpgME::GpgConfEngine); /** Returns true, if GnuPG knows which keyserver to use for keyserver * operations. + * + * Since GnuPG 2.2.42/2.4.4 dirmngr supports the special value "none" + * to disable usage of the default keyserver. If this value is configured + * and GnuPG is new enough then this function returns false. * Since version 2.1.19 GnuPG has a builtin default keyserver, so that this - * function always returns true. For older versions of GnuPG it checks if - * a keyserver has been configured. + * function always returns true (unless the above applies). + * For older versions of GnuPG it checks if a keyserver has been configured. */ KLEO_EXPORT bool haveKeyserverConfigured(); /** Returns the configured keyserver or an empty string if no keyserver is - * configured. + * configured. The special value "none" indicates that no keyserver shall + * be used. + * * Note: Since GnuPG 2.1.19 gpg/dirmngr uses a default keyserver if no * keyserver is configured. + * Since GnuPG 2.2.42/2.4.4 dirmngr supports the special value "none" + * to disable usage of the default keyserver. */ KLEO_EXPORT QString keyserver(); /** Returns true, if GnuPG knows which server to use for directory service * operations for X.509 certificates. */ KLEO_EXPORT bool haveX509DirectoryServerConfigured(); /* Use gnupgUsesDeVsCompliance() or gnupgIsDeVsCompliant() instead. */ KLEO_DEPRECATED_EXPORT bool gpgComplianceP(const char *mode); /** * Use Kleo::DeVSCompliance::isActive() instead. */ KLEO_DEPRECATED_EXPORT bool gnupgUsesDeVsCompliance(); /** * Use Kleo::DeVSCompliance::isCompliant() instead. */ KLEO_DEPRECATED_EXPORT bool gnupgIsDeVsCompliant(); /* Convert GnuPG output to a QString with proper encoding. * Takes Gpg Quirks into account and might handle future * changes in GnuPG Output. */ KLEO_EXPORT QString stringFromGpgOutput(const QByteArray &ba); /* Check if a minimum version is there. Strings should be in the format: * 1.2.3 */ KLEO_EXPORT bool versionIsAtLeast(const char *minimum, const char *actual); /** Returns a list of component names (e.g. GnuPG, libgcrypt) followed by * version numbers. This is meant for displaying in the About dialog. */ KLEO_EXPORT QStringList backendVersionInfo(); /** Launch the GnuPG agent if it is not already running. */ KLEO_EXPORT void launchGpgAgent(); /** Shut down all GnuPG daemons. They will be restarted automatically when * needed. */ KLEO_EXPORT void killDaemons(); /** * Returns a static list of the available algorithms. */ KLEO_EXPORT const std::vector &availableAlgorithms(); /** * Returns a static list of the preferred algorithms with decreasing preference. */ KLEO_EXPORT const std::vector &preferredAlgorithms(); /** * Returns a static list of algorithms that are explicitly not supported. */ KLEO_EXPORT const std::vector &ignoredAlgorithms(); } diff --git a/src/utils/keyhelpers.cpp b/src/utils/keyhelpers.cpp index 7df4ba545..f1bcbe2fe 100644 --- a/src/utils/keyhelpers.cpp +++ b/src/utils/keyhelpers.cpp @@ -1,294 +1,308 @@ /* utils/keyhelpers.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keyhelpers.h" #include #include #include #include #include // needed for GPGME_VERSION_NUMBER #include #include using namespace Kleo; using namespace GpgME; namespace { bool havePublicKeyForSignature(const GpgME::UserID::Signature &signature) { // GnuPG returns status "NoPublicKey" for missing signing keys, but also // for expired or revoked signing keys. return (signature.status() != GpgME::UserID::Signature::NoPublicKey) // || !KeyCache::instance()->findByKeyIDOrFingerprint(signature.signerKeyID()).isNull(); } auto _getMissingSignerKeyIds(const std::vector &signatures) { return std::accumulate(std::begin(signatures), std::end(signatures), std::set{}, [](auto &keyIds, const auto &signature) { if (!havePublicKeyForSignature(signature)) { keyIds.insert(QLatin1String{signature.signerKeyID()}); } return keyIds; }); } } std::set Kleo::getMissingSignerKeyIds(const std::vector &userIds) { return std::accumulate(std::begin(userIds), std::end(userIds), std::set{}, [](auto &keyIds, const auto &userID) { if (!userID.isBad()) { const auto newKeyIds = _getMissingSignerKeyIds(userID.signatures()); std::copy(std::begin(newKeyIds), std::end(newKeyIds), std::inserter(keyIds, std::end(keyIds))); } return keyIds; }); } std::set Kleo::getMissingSignerKeyIds(const std::vector &keys) { return std::accumulate(std::begin(keys), std::end(keys), std::set{}, [](auto &keyIds, const auto &key) { if (!key.isBad()) { const auto newKeyIds = getMissingSignerKeyIds(key.userIDs()); std::copy(std::begin(newKeyIds), std::end(newKeyIds), std::inserter(keyIds, std::end(keyIds))); } return keyIds; }); } bool Kleo::isRemoteKey(const GpgME::Key &key) { // a remote key looked up via WKD has key list mode Local; therefore we also look for the key in the local key ring return (key.keyListMode() == GpgME::Extern) || KeyCache::instance()->findByFingerprint(key.primaryFingerprint()).isNull(); } GpgME::UserID::Validity Kleo::minimalValidityOfNotRevokedUserIDs(const Key &key) { const std::vector userIDs = key.userIDs(); const int minValidity = std::accumulate(userIDs.begin(), userIDs.end(), UserID::Ultimate + 1, [](int validity, const UserID &userID) { return userID.isRevoked() ? validity : std::min(validity, static_cast(userID.validity())); }); return minValidity <= UserID::Ultimate ? static_cast(minValidity) : UserID::Unknown; } GpgME::UserID::Validity Kleo::maximalValidityOfUserIDs(const Key &key) { const auto userIDs = key.userIDs(); const int maxValidity = std::accumulate(userIDs.begin(), userIDs.end(), 0, [](int validity, const UserID &userID) { return std::max(validity, static_cast(userID.validity())); }); return static_cast(maxValidity); } bool Kleo::allUserIDsHaveFullValidity(const GpgME::Key &key) { return minimalValidityOfNotRevokedUserIDs(key) >= UserID::Full; } namespace { bool isLastValidUserID(const GpgME::UserID &userId) { if (Kleo::isRevokedOrExpired(userId)) { return false; } const auto userIds = userId.parent().userIDs(); const int numberOfValidUserIds = std::count_if(std::begin(userIds), std::end(userIds), [](const auto &u) { return !Kleo::isRevokedOrExpired(u); }); return numberOfValidUserIds == 1; } bool hasValidUserID(const GpgME::Key &key) { return Kleo::any_of(key.userIDs(), [](const auto &u) { return !Kleo::isRevokedOrExpired(u); }); } } bool Kleo::isSelfSignature(const GpgME::UserID::Signature &signature) { return !qstrcmp(signature.parent().parent().keyID(), signature.signerKeyID()); } bool Kleo::isRevokedOrExpired(const GpgME::UserID &userId) { if (userId.isRevoked() || userId.parent().isExpired()) { return true; } const auto sigs = userId.signatures(); std::vector selfSigs; std::copy_if(std::begin(sigs), std::end(sigs), std::back_inserter(selfSigs), &Kleo::isSelfSignature); std::sort(std::begin(selfSigs), std::end(selfSigs)); // check the most recent signature const auto sig = !selfSigs.empty() ? selfSigs.back() : GpgME::UserID::Signature{}; return !sig.isNull() && (sig.isRevokation() || sig.isExpired()); } +bool Kleo::isExpired(const UserID &userID) +{ + if (userID.parent().isExpired()) { + return true; + } + const auto sigs = userID.signatures(); + std::vector selfSigs; + std::copy_if(std::begin(sigs), std::end(sigs), std::back_inserter(selfSigs), &Kleo::isSelfSignature); + std::sort(std::begin(selfSigs), std::end(selfSigs)); + // check the most recent signature + const auto sig = !selfSigs.empty() ? selfSigs.back() : GpgME::UserID::Signature{}; + return !sig.isNull() && sig.isExpired(); +} + bool Kleo::canCreateCertifications(const GpgME::Key &key) { return Kleo::keyHasCertify(key) && canBeUsedForSecretKeyOperations(key); } bool Kleo::canBeCertified(const GpgME::Key &key) { return key.protocol() == GpgME::OpenPGP // && !key.isBad() // && hasValidUserID(key); } namespace { static inline bool subkeyHasSecret(const GpgME::Subkey &subkey) { #if GPGME_VERSION_NUMBER >= 0x011102 // 1.17.2 // we need to check the primary subkey because Key::hasSecret() is also true if just the secret key stub of an offline key is available return subkey.isSecret(); #else // older versions of GpgME did not always set the secret flag for card keys return subkey.isSecret() || subkey.isCardKey(); #endif } } bool Kleo::canBeUsedForEncryption(const GpgME::Key &key) { return !key.isBad() && Kleo::any_of(key.subkeys(), [](const auto &subkey) { return subkey.canEncrypt() && !subkey.isBad(); }); } bool Kleo::canBeUsedForSigning(const GpgME::Key &key) { return !key.isBad() && Kleo::any_of(key.subkeys(), [](const auto &subkey) { return subkey.canSign() && !subkey.isBad() && subkeyHasSecret(subkey); }); } bool Kleo::canBeUsedForSecretKeyOperations(const GpgME::Key &key) { return subkeyHasSecret(key.subkey(0)); } bool Kleo::canRevokeUserID(const GpgME::UserID &userId) { return (!userId.isNull() // && userId.parent().protocol() == GpgME::OpenPGP // && !isLastValidUserID(userId)); } bool Kleo::isSecretKeyStoredInKeyRing(const GpgME::Key &key) { return key.subkey(0).isSecret() && !key.subkey(0).isCardKey(); } bool Kleo::userHasCertificationKey() { const auto secretKeys = KeyCache::instance()->secretKeys(); return Kleo::any_of(secretKeys, [](const auto &k) { return (k.protocol() == GpgME::OpenPGP) && canCreateCertifications(k); }); } Kleo::CertificationRevocationFeasibility Kleo::userCanRevokeCertification(const GpgME::UserID::Signature &certification) { const auto certificationKey = KeyCache::instance()->findByKeyIDOrFingerprint(certification.signerKeyID()); const bool isSelfSignature = qstrcmp(certification.parent().parent().keyID(), certification.signerKeyID()) == 0; if (!certificationKey.hasSecret()) { return CertificationNotMadeWithOwnKey; } else if (isSelfSignature) { return CertificationIsSelfSignature; } else if (certification.isRevokation()) { return CertificationIsRevocation; } else if (certification.isExpired()) { return CertificationIsExpired; } else if (certification.isInvalid()) { return CertificationIsInvalid; } else if (!canCreateCertifications(certificationKey)) { return CertificationKeyNotAvailable; } return CertificationCanBeRevoked; } bool Kleo::userCanRevokeCertifications(const GpgME::UserID &userId) { if (userId.numSignatures() == 0) { qCWarning(LIBKLEO_LOG) << __func__ << "- Error: Signatures of user ID" << QString::fromUtf8(userId.id()) << "not available"; } return Kleo::any_of(userId.signatures(), [](const auto &certification) { return userCanRevokeCertification(certification) == CertificationCanBeRevoked; }); } bool Kleo::userIDBelongsToKey(const GpgME::UserID &userID, const GpgME::Key &key) { return !qstricmp(userID.parent().primaryFingerprint(), key.primaryFingerprint()); } static time_t creationDate(const GpgME::UserID &uid) { // returns the date of the first self-signature for (unsigned int i = 0, numSignatures = uid.numSignatures(); i < numSignatures; ++i) { const auto sig = uid.signature(i); if (Kleo::isSelfSignature(sig)) { return sig.creationTime(); } } return 0; } bool Kleo::userIDsAreEqual(const GpgME::UserID &lhs, const GpgME::UserID &rhs) { return (qstrcmp(lhs.parent().primaryFingerprint(), rhs.parent().primaryFingerprint()) == 0 // && qstrcmp(lhs.id(), rhs.id()) == 0 // && creationDate(lhs) == creationDate(rhs)); } static inline bool isOpenPGPCertification(const GpgME::UserID::Signature &sig) { // certification class is 0x10, ..., 0x13 return (sig.certClass() & ~0x03) == 0x10; } static bool isOpenPGPCertificationByUser(const GpgME::UserID::Signature &sig) { if (!isOpenPGPCertification(sig)) { return false; } const auto certificationKey = KeyCache::instance()->findByKeyIDOrFingerprint(sig.signerKeyID()); return certificationKey.ownerTrust() == Key::Ultimate; } bool Kleo::userIDIsCertifiedByUser(const GpgME::UserID &userId) { if (userId.parent().protocol() != GpgME::OpenPGP) { qCWarning(LIBKLEO_LOG) << __func__ << "not called with OpenPGP key"; return false; } if (userId.numSignatures() == 0) { qCWarning(LIBKLEO_LOG) << __func__ << "- Error: Signatures of user ID" << QString::fromUtf8(userId.id()) << "not available"; } for (unsigned int i = 0, numSignatures = userId.numSignatures(); i < numSignatures; ++i) { const auto sig = userId.signature(i); if ((sig.status() == UserID::Signature::NoError) && !sig.isBad() && sig.isExportable() && isOpenPGPCertificationByUser(sig)) { return true; } } return false; } diff --git a/src/utils/keyhelpers.h b/src/utils/keyhelpers.h index 683457495..0bbf4bc9f 100644 --- a/src/utils/keyhelpers.h +++ b/src/utils/keyhelpers.h @@ -1,208 +1,211 @@ /* utils/keyhelpers.h This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2021-2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include #include #include #include namespace Kleo { template QStringList getFingerprints(const KeyContainer &keys) { QStringList fingerprints; fingerprints.reserve(keys.size()); std::transform(std::begin(keys), std::end(keys), std::back_inserter(fingerprints), [](const auto &key) { return QString::fromLatin1(key.primaryFingerprint()); }); return fingerprints; } KLEO_EXPORT std::set getMissingSignerKeyIds(const std::vector &userIds); KLEO_EXPORT std::set getMissingSignerKeyIds(const std::vector &keys); /** * Returns true, if the key \p key is the result of a lookup which is not present * in the local key ring. */ KLEO_EXPORT bool isRemoteKey(const GpgME::Key &key); KLEO_EXPORT GpgME::UserID::Validity minimalValidityOfNotRevokedUserIDs(const GpgME::Key &key); KLEO_EXPORT GpgME::UserID::Validity maximalValidityOfUserIDs(const GpgME::Key &key); /* Is the key valid i.e. are all not revoked uids fully trusted? */ KLEO_EXPORT bool allUserIDsHaveFullValidity(const GpgME::Key &key); template bool allKeysHaveProtocol(const RangeOfKeys &keys, GpgME::Protocol protocol) { return std::all_of(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); } template bool anyKeyHasProtocol(const RangeOfKeys &keys, GpgME::Protocol protocol) { return std::any_of(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); } /** Returns true if \p signature is a self-signature. */ KLEO_EXPORT bool isSelfSignature(const GpgME::UserID::Signature &signature); /** * Returns true if the most recent self-signature of \p userId is a revocation * signature or if it has expired. */ KLEO_EXPORT bool isRevokedOrExpired(const GpgME::UserID &userId); +/** Returns true if the most recent self-signature of \p userId has expired. */ +KLEO_EXPORT bool isExpired(const GpgME::UserID &userId); + /** * Returns true if \p key can be used to certify user IDs, i.e. if the key * has the required capability and if the secret key of the (primary) * certification subkey is available in the keyring or on a smart card. */ KLEO_EXPORT bool canCreateCertifications(const GpgME::Key &key); /** * Returns true if the key \p key can be certified, i.e. it is an OpenPGP key * which is neither revoked nor expired and which has at least one user ID * that is neither revoked nor expired. */ KLEO_EXPORT bool canBeCertified(const GpgME::Key &key); /** * Returns true if the certificate \p key can be used for encryption, i.e. if * it has at least one encryption subkey that is neither expired nor revoked * nor otherwise invalid. */ KLEO_EXPORT bool canBeUsedForEncryption(const GpgME::Key &key); /** * Returns true if the certificate \p key can be used for signing data, i.e. if * it has at least one signing subkey that is neither expired nor revoked * nor otherwise invalid and for which the secret key is available. */ KLEO_EXPORT bool canBeUsedForSigning(const GpgME::Key &key); /** * Returns true if \p key can be used for operations requiring the secret key, * i.e. if the secret key of the primary key pair is available in the keyring * or on a smart card. * * \note Key::hasSecret() also returns true if a secret key stub, e.g. of an * offline key, is available in the keyring. */ KLEO_EXPORT bool canBeUsedForSecretKeyOperations(const GpgME::Key &key); /** * Returns true if \p userId can be revoked, i.e. if it isn't the last valid * user ID of an OpenPGP key. */ KLEO_EXPORT bool canRevokeUserID(const GpgME::UserID &userId); /** * Returns true if the secret key of the primary key pair of \p key is stored * in the keyring. */ KLEO_EXPORT bool isSecretKeyStoredInKeyRing(const GpgME::Key &key); /** * Returns true if any keys suitable for certifying user IDs are available in * the keyring or on a smart card. * * \sa canCreateCertifications */ KLEO_EXPORT bool userHasCertificationKey(); enum CertificationRevocationFeasibility { CertificationCanBeRevoked = 0, CertificationNotMadeWithOwnKey, CertificationIsSelfSignature, CertificationIsRevocation, CertificationIsExpired, CertificationIsInvalid, CertificationKeyNotAvailable, }; /** * Checks if the user can revoke the given \p certification. */ KLEO_EXPORT CertificationRevocationFeasibility userCanRevokeCertification(const GpgME::UserID::Signature &certification); /** * Returns true if the user can revoke any of the certifications of the \p userId. * * \sa userCanRevokeCertification */ KLEO_EXPORT bool userCanRevokeCertifications(const GpgME::UserID &userId); /** * Returns true, if the user ID \p userID belongs to the key \p key. */ KLEO_EXPORT bool userIDBelongsToKey(const GpgME::UserID &userID, const GpgME::Key &key); /** * Returns a unary predicate to check if a user ID belongs to the key \p key. */ inline auto userIDBelongsToKey(const GpgME::Key &key) { return [key](const GpgME::UserID &userID) { return userIDBelongsToKey(userID, key); }; } /** * Returns true, if the two user IDs \p lhs and \p rhs are equal. * * Equality means that both user IDs belong to the same key, contain identical * text, and have the same creation date (i.e. the creation date of the first * self-signature is the same). */ KLEO_EXPORT bool userIDsAreEqual(const GpgME::UserID &lhs, const GpgME::UserID &rhs); /** * Returns true, if the user ID \p userId has a valid, exportable certification * that was made with one of the available ultimately trusted OpenPGP keys. */ KLEO_EXPORT bool userIDIsCertifiedByUser(const GpgME::UserID &userId); struct KLEO_EXPORT KeysByProtocol { std::vector openpgp; std::vector cms; }; /** * Partitions the keys \p keys into OpenPGP keys and CMS certificates. */ template KeysByProtocol partitionKeysByProtocol(const KeyContainer &keys) { KeysByProtocol result; std::partition_copy(std::begin(keys), std::end(keys), std::back_inserter(result.openpgp), std::back_inserter(result.cms), [](const auto &key) { return key.protocol() == GpgME::OpenPGP; }); return result; } } diff --git a/src/utils/stringutils.cpp b/src/utils/stringutils.cpp index 48f37cec1..62d92f18e 100644 --- a/src/utils/stringutils.cpp +++ b/src/utils/stringutils.cpp @@ -1,29 +1,29 @@ /* - utils/string.cpp + utils/stringutils.cpp This file is part of libkleopatra SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "stringutils.h" std::vector Kleo::split(const std::string &s, char c) { std::vector result; auto start = 0; auto end = s.find(c, start); while (end != s.npos) { result.push_back(s.substr(start, end - start)); start = end + 1; end = s.find(c, start); } result.push_back(s.substr(start)); return result; } diff --git a/src/utils/stringutils.h b/src/utils/stringutils.h index 513ba9172..6fc2bb513 100644 --- a/src/utils/stringutils.h +++ b/src/utils/stringutils.h @@ -1,38 +1,38 @@ /* - utils/string.h + utils/stringutils.h This file is part of libkleopatra SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "kleo_export.h" #include #include #include namespace Kleo { /** Splits the string @p s into substrings wherever the character @p c occurs, * and returns the list of those strings. */ KLEO_EXPORT std::vector split(const std::string &s, char c); /** * Returns true if the string @p sv begins with the string @p prefix, false * otherwise. */ inline bool startsWith(std::string_view sv, std::string_view prefix) { #ifdef __cpp_lib_starts_ends_with return sv.starts_with(prefix); #else return sv.substr(0, prefix.size()) == prefix; #endif } }