diff --git a/CMakeLists.txt b/CMakeLists.txt index d01380dc8..52e0f75ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,198 +1,201 @@ # SPDX-License-Identifier: CC0-1.0 # SPDX-FileCopyrightText: none cmake_minimum_required(VERSION 3.16 FATAL_ERROR) set(PIM_VERSION "5.23.43") project(libkleo VERSION ${PIM_VERSION}) set(KF_MIN_VERSION "5.105.0") if (WIN32) set(KF5_WANT_VERSION "5.70.0") else () set(KF5_WANT_VERSION ${KF_MIN_VERSION}) endif () find_package(ECM ${KF5_WANT_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMGenerateExportHeader) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMGeneratePriFile) include(FeatureSummary) include(ECMQtDeclareLoggingCategory) include(ECMDeprecationSettings) include(ECMAddQch) include(KDEClangFormat) include(KDEGitCommitHooks) option(BUILD_WITH_COMPAT_LIBS "Generate compat cmake file (Set at false will allow to co-install lib)" ON) option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") set(LIBKLEO_LIB_VERSION ${PIM_VERSION}) set(QT_REQUIRED_VERSION "5.15.2") if (QT_MAJOR_VERSION STREQUAL "6") set(QT_REQUIRED_VERSION "6.4.0") set(KF_MIN_VERSION "5.240.0") set(KF_MAJOR_VERSION "6") else() set(KF_MAJOR_VERSION "5") endif() set(KDEPIMTEXTEDIT_VERSION "5.23.40") set(GPGME_REQUIRED_VERSION "1.16.0") find_package(Qt${QT_MAJOR_VERSION} ${QT_REQUIRED_VERSION} CONFIG REQUIRED Widgets) if (QT_MAJOR_VERSION STREQUAL "6") find_package(Qt6Core5Compat) endif() find_package(KF${KF_MAJOR_VERSION}I18n ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KF${KF_MAJOR_VERSION}Config ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KF${KF_MAJOR_VERSION}WidgetsAddons ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KF${KF_MAJOR_VERSION}ConfigWidgets ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KF${KF_MAJOR_VERSION}Completion ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KF${KF_MAJOR_VERSION}CoreAddons ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KF${KF_MAJOR_VERSION}Codecs ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KF${KF_MAJOR_VERSION}ItemModels ${KF5_WANT_VERSION} CONFIG REQUIRED) find_package(KPim${KF_MAJOR_VERSION}TextEdit ${KDEPIMTEXTEDIT_VERSION} CONFIG) find_package(Gpgmepp ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) set_package_properties(Gpgmepp PROPERTIES DESCRIPTION "GpgME++ Library" URL "https://www.gnupg.org" TYPE REQUIRED PURPOSE "GpgME++ is required for OpenPGP support") +if (Gpgmepp_VERSION VERSION_GREATER_EQUAL "1.19.1") + set(GPGMEPP_KEY_CANSIGN_IS_FIXED 1) +endif() if (QT_MAJOR_VERSION STREQUAL "6") find_package(QGpgmeQt6 ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) else() find_package(QGpgme ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED) endif() message(STATUS "GpgME++ Version ${Gpgmepp_VERSION}") if (QGpgme_VERSION VERSION_GREATER_EQUAL "1.18.1") set(QGPGME_LISTALLKEYSJOB_HAS_OPTIONS 1) endif() find_package(Boost 1.34.0) set_package_properties(Boost PROPERTIES DESCRIPTION "Boost C++ Libraries" URL "https://www.boost.org" TYPE REQUIRED PURPOSE "Boost is required for building most KDEPIM applications") set_package_properties(KPim${KF_MAJOR_VERSION}TextEdit PROPERTIES DESCRIPTION "A textedit with PIM-specific features." URL "https://commits.kde.org/kpimtextedit" TYPE OPTIONAL PURPOSE "Improved audit log viewer.") ecm_setup_version(PROJECT VARIABLE_PREFIX LIBKLEO VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/libkleo_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KPim${KF_MAJOR_VERSION}LibkleoConfigVersion.cmake" SOVERSION 5 ) ########### Targets ########### ecm_set_disabled_deprecation_versions(QT 5.15.2 KF 5.105.0) remove_definitions(-DQT_NO_FOREACH) add_definitions(-DQT_NO_EMIT) ########### CMake Config Files ########### set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KPim${KF_MAJOR_VERSION}Libkleo") set(LIBKLEO_KF5_COMPAT FALSE) configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KPimLibkleoConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KPim${KF_MAJOR_VERSION}LibkleoConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KPim${KF_MAJOR_VERSION}LibkleoConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KPim${KF_MAJOR_VERSION}LibkleoConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(EXPORT KPim${KF_MAJOR_VERSION}LibkleoTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KPim${KF_MAJOR_VERSION}LibkleoTargets.cmake NAMESPACE KPim${KF_MAJOR_VERSION}::) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libkleo_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KPim${KF_MAJOR_VERSION}/Libkleo COMPONENT Devel ) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-libkleo.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-libkleo.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) option(USE_UNITY_CMAKE_SUPPORT "Use UNITY cmake support (speedup compile time)" OFF) set(COMPILE_WITH_UNITY_CMAKE_SUPPORT OFF) if (USE_UNITY_CMAKE_SUPPORT) set(COMPILE_WITH_UNITY_CMAKE_SUPPORT ON) endif() add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(autotests) add_subdirectory(tests) endif() ecm_qt_install_logging_categories( EXPORT LIBKLEO FILE libkleo.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ) ki18n_install(po) if (BUILD_QCH) ecm_install_qch_export( TARGETS KPim${KF_MAJOR_VERSION}Libkleo_QCH FILE KPim${KF_MAJOR_VERSION}LibkleoQchTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KPim${KF_MAJOR_VERSION}LibkleoQchTargets.cmake\")") endif() feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) # add clang-format target for all our real source files file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h *.c) kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) if (BUILD_WITH_COMPAT_LIBS) #if (QT_MAJOR_VERSION STREQUAL "5") ## # TODO: Backwards compatiblity. Remove in next major version ## set(CMAKECONFIG_INSTALL_DIR_KF5 "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Libkleo") set(LIBKLEO_KF5_COMPAT TRUE) configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KPimLibkleoConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5LibkleoConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR_KF5} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5LibkleoConfig.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR_KF5}" COMPONENT Devel ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KPim${KF_MAJOR_VERSION}LibkleoConfigVersion.cmake" RENAME "KF5LibkleoConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR_KF5}" COMPONENT Devel ) install(EXPORT KPim${KF_MAJOR_VERSION}LibkleoTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR_KF5}" FILE KPim${KF_MAJOR_VERSION}LibkleoTargets.cmake NAMESPACE KF5:: ) if (BUILD_QCH) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KPim${KF_MAJOR_VERSION}LibkleoQchTargets.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR_KF5}" COMPONENT Devel ) endif() #endif() endif() diff --git a/config-libkleo.h.cmake b/config-libkleo.h.cmake index b82ded531..23405dd7f 100644 --- a/config-libkleo.h.cmake +++ b/config-libkleo.h.cmake @@ -1,2 +1,5 @@ /* Defined if QGpgME::ListAllKeysJob supports setting options */ #cmakedefine QGPGME_LISTALLKEYSJOB_HAS_OPTIONS 1 + +/* Defined if Key::canSign should be used instead of deprecated Key::canReallySign */ +#cmakedefine01 GPGMEPP_KEY_CANSIGN_IS_FIXED diff --git a/src/models/keycache.cpp b/src/models/keycache.cpp index e54fb7dbf..41b15e17d 100644 --- a/src/models/keycache.cpp +++ b/src/models/keycache.cpp @@ -1,1742 +1,1742 @@ /* -*- 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 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 { connect(d->m_refreshJob.data(), &RefreshKeysJob::done, this, [this](const GpgME::KeyListResult &) { qCDebug(LIBKLEO_LOG) << "Reloading keycache with remarks enabled"; QTimer::singleShot(1s, this, [this]() { 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); 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 { -#if 1 ACCEPT(hasSecret); +#if GPGMEPP_KEY_CANSIGN_IS_FIXED + ACCEPT(canSign); +#else ACCEPT(canReallySign); +#endif REJECT(isRevoked); REJECT(isExpired); REJECT(isDisabled); REJECT(isInvalid); return true; -#else - return key.hasSecret() && key.canReallySign() && !key.isRevoked() && !key.isExpired() && !key.isDisabled() && !key.isInvalid(); -#endif #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 ACCEPT(canEncrypt); REJECT(isRevoked); REJECT(isExpired); REJECT(isDisabled); REJECT(isInvalid); return true; #else return key.canEncrypt() && !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 { 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 KeyCache::findSubjects(std::vector::const_iterator first, std::vector::const_iterator last, Options options) const { if (first == last) { return std::vector(); } std::vector result; while (first != last) { const auto pair = d->find_subjects(first->primaryFingerprint()); result.insert(result.end(), pair.first, pair.second); ++first; } std::sort(result.begin(), result.end(), _detail::ByFingerprint()); result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); 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); } 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()); if (issuer.isNull()) { return result; } result.push_back(issuer); if (!(options & RecursiveSearch)) { return result; } while (true) { const Key &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 result.push_back(issuer); if (issuer.isRoot() || chainAlreadyContainsIssuer) { 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; } Q_EMIT aboutToRemove(key); { 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 added(key); } 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; QVector 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(); } #ifdef QGPGME_LISTALLKEYSJOB_HAS_OPTIONS if (!m_cache->initialized()) { // avoid delays during the initial key listing job->setOptions(QGpgME::ListAllKeysJob::DisableAutomaticTrustDatabaseCheck); } #endif #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 && !k.canEncrypt()) { continue; } if (usage == KeyUsage::Sign && (!k.canSign() || !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), std::mem_fn(&Key::canSign)); case KeyCache::KeyUsage::Encrypt: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canEncrypt)); case KeyCache::KeyUsage::Certify: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canCertify)); case KeyCache::KeyUsage::Authenticate: return std::all_of(std::begin(keys), std::end(keys), std::mem_fn(&Key::canAuthenticate)); } qCDebug(LIBKLEO_LOG) << __func__ << "called with invalid usage" << int(usage); return false; } template bool allKeysHaveProtocol(const T &keys, Protocol protocol) { return std::all_of(std::begin(keys), std::end(keys), [protocol](const auto &key) { return key.protocol() == protocol; }); } } 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/utils/formatting.cpp b/src/utils/formatting.cpp index 7cb8ffd0b..82b590936 100644 --- a/src/utils/formatting.cpp +++ b/src/utils/formatting.cpp @@ -1,1301 +1,1305 @@ /* -*- 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 "compliance.h" #include "cryptoconfig.h" #include "gnupg.h" #include "keyhelpers.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // for Qt::escape #include #include using namespace GpgME; using namespace Kleo; // // 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 GPGMEPP_KEY_CANSIGN_IS_FIXED + if (key.canSign()) { +#else if (key.canReallySign()) { +#endif if (key.isQualified()) { capabilities.push_back(i18n("Signing (Qualified)")); } else { capabilities.push_back(i18n("Signing")); } } if (key.canEncrypt()) { capabilities.push_back(i18n("Encryption")); } if (key.canCertify()) { capabilities.push_back(i18n("Certifying User-IDs")); } if (key.canAuthenticate()) { 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()); } } Q_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 QIcon::fromTheme(QStringLiteral("emblem-success")); case 0x30: return QIcon::fromTheme(QStringLiteral("emblem-error")); default: return QIcon(); } } Q_FALLTHROUGH(); // fall through: case UserID::Signature::BadSignature: case UserID::Signature::GeneralError: return QIcon::fromTheme(QStringLiteral("emblem-error")); case UserID::Signature::SigExpired: case UserID::Signature::KeyExpired: return QIcon::fromTheme(QStringLiteral("emblem-information")); case UserID::Signature::NoPublicKey: return QIcon::fromTheme(QStringLiteral("emblem-question")); } 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(); } namespace { static QString keyToString(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), QString::fromLocal8Bit(sig.status().asString())); } else { return i18n("Bad signature by an unknown certificate: %1", QString::fromLocal8Bit(sig.status().asString())); } } else { return i18n("Bad signature by %1: %2", keyToString(key), QString::fromLocal8Bit(sig.status().asString())); } } 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.", keyToString(key)); } } else if (key.isNull()) { if (const char *fpr = sig.fingerprint()) { return i18n("Invalid signature by unknown certificate %1: %2", QString::fromLatin1(fpr), QString::fromLocal8Bit(sig.status().asString())); } else { return i18n("Invalid signature by an unknown certificate: %1", QString::fromLocal8Bit(sig.status().asString())); } } else { return i18n("Invalid signature by %1: %2", keyToString(key), QString::fromLocal8Bit(sig.status().asString())); } } // // 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", QString::fromLocal8Bit(import.error().asString())); } 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"); } return usageStrings.join(QLatin1String(", ")); } QString Formatting::summaryLine(const Key &key) { return keyToString(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)); } } namespace { QIcon iconForValidity(UserID::Validity validity) { switch (validity) { case UserID::Ultimate: case UserID::Full: case UserID::Marginal: return QIcon::fromTheme(QStringLiteral("emblem-success")); case UserID::Never: return QIcon::fromTheme(QStringLiteral("emblem-error")); case UserID::Undefined: case UserID::Unknown: default: return QIcon::fromTheme(QStringLiteral("emblem-information")); } } } // Icon for certificate selection indication QIcon Formatting::iconForUid(const UserID &uid) { return iconForValidity(uid.validity()); } 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; } } QIcon Formatting::validityIcon(const KeyGroup &group) { return iconForValidity(minimalValidity(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::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 {}; } }