diff --git a/src/view/cardkeysview.cpp b/src/view/cardkeysview.cpp index bb0d59627..7d21039b5 100644 --- a/src/view/cardkeysview.cpp +++ b/src/view/cardkeysview.cpp @@ -1,578 +1,584 @@ /* view/cardkeysview.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2024 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #include "cardkeysview.h" #include <tooltippreferences.h> #include <kleopatra_debug.h> #include <commands/detailscommand.h> #include <smartcard/card.h> #include <smartcard/readerstatus.h> #include <utils/gui-helper.h> #include <view/progressoverlay.h> #include <view/smartcardactions.h> #include <Libkleo/Debug> #include <Libkleo/Dn> #include <Libkleo/Formatting> #include <Libkleo/KeyCache> #include <Libkleo/KeyHelpers> #include <Libkleo/TreeWidget> #include <KConfigGroup> #include <KLocalizedString> #include <KSharedConfig> #include <QGpgME/KeyListJob> #include <QGpgME/Protocol> #include <QHeaderView> #include <QLabel> #include <QMenu> #include <QToolButton> #include <QVBoxLayout> #include <gpgme++/context.h> #include <gpgme++/key.h> #include <gpgme++/keylistresult.h> #include <algorithm> using namespace GpgME; using namespace Kleo; using namespace Kleo::SmartCard; using namespace Kleo::Commands; using namespace Qt::Literals::StringLiterals; static int toolTipOptions() { using namespace Kleo::Formatting; static const int validityFlags = Validity | Issuer | ExpiryDates | CertificateUsage; static const int ownerFlags = Subject | UserIDs | OwnerTrust; static const int detailsFlags = StorageLocation | CertificateType | SerialNumber | Fingerprint; const TooltipPreferences prefs; int flags = KeyID; flags |= prefs.showValidity() ? validityFlags : 0; flags |= prefs.showOwnerInformation() ? ownerFlags : 0; flags |= prefs.showCertificateDetails() ? detailsFlags : 0; return flags; } namespace { enum ColumnIndex { Slot, KeyGrip, Usage, Created, Fingerprint, Certificate, Actions, }; } namespace { static const int CardKeysWidgetItemType = QTreeWidgetItem::UserType; class CardKeysWidgetItem : public QTreeWidgetItem { public: CardKeysWidgetItem(int slotIndex, const std::string &keyRef) : QTreeWidgetItem{CardKeysWidgetItemType} , mSlotIndex{slotIndex} , mKeyRef{keyRef} { } ~CardKeysWidgetItem() override = default; int slotIndex() const { return mSlotIndex; } const std::string &keyRef() const { return mKeyRef; } void setSubkey(const Subkey &subkey) { mSubkey = subkey; } const Subkey &subkey() const { return mSubkey; } private: int mSlotIndex; std::string mKeyRef; Subkey mSubkey; }; } static QString cardKeyUsageDisplayName(char c) { switch (c) { case 'e': return i18n("encryption"); case 's': return i18n("signatures"); case 'c': return i18n("certification"); case 'a': return i18n("authentication"); default: return {}; }; } static QStringList cardKeyUsageDisplayNames(const std::string &usage) { QStringList result; if (usage == "-") { // special case (e.g. for some NetKey keys) return result; } result.reserve(usage.size()); std::ranges::transform(usage, std::back_inserter(result), &cardKeyUsageDisplayName); return result; } static std::vector<CardKeysWidgetItem *> getItems(const TreeWidget *treeWidget, int slotIndex) { std::vector<CardKeysWidgetItem *> items; for (int i = 0; i < treeWidget->topLevelItemCount(); ++i) { auto item = static_cast<CardKeysWidgetItem *>(treeWidget->topLevelItem(i)); if (item->slotIndex() == slotIndex) { items.push_back(item); } else if (item->slotIndex() > slotIndex) { // the items are sorted by slot index so that we do not have to look further break; } } return items; } static void updateTreeWidgetItem(CardKeysWidgetItem *item, const KeyPairInfo &keyInfo, const Subkey &subkey, CardKeysView::Options options) { Q_ASSERT(item); // slot item->setData(Slot, Qt::DisplayRole, QString::number(item->slotIndex() + 1)); // key grip item->setData(KeyGrip, Qt::DisplayRole, Formatting::prettyID(keyInfo.grip.c_str())); item->setData(KeyGrip, Qt::AccessibleTextRole, Formatting::accessibleHexID(keyInfo.grip.c_str())); // usage auto usages = cardKeyUsageDisplayNames(keyInfo.usage); if (usages.empty()) { item->setData(Usage, Qt::DisplayRole, QString::fromStdString(keyInfo.usage)); item->setData(Usage, Qt::AccessibleTextRole, i18nc("@info entry in Usage column of a smart card key", "none")); } else { item->setData(Usage, Qt::DisplayRole, usages.join(i18nc("Separator between words in a list", ", "))); } // created if (!(options & CardKeysView::NoCreated)) { item->setData(Created, Qt::DisplayRole, QString::fromStdString(keyInfo.keyTime)); } item->setSubkey(subkey); if (subkey.isNull()) { // fingerprint item->setData(Fingerprint, Qt::DisplayRole, QString{}); // certificate item->setData(Certificate, Qt::DisplayRole, QString{}); } else { // fingerprint item->setData(Fingerprint, Qt::DisplayRole, Formatting::prettyID(subkey.fingerprint())); item->setData(Fingerprint, Qt::AccessibleTextRole, Formatting::accessibleHexID(subkey.fingerprint())); // certificate if (subkey.parent().protocol() == GpgME::OpenPGP) { item->setData(Certificate, Qt::DisplayRole, Formatting::prettyUserID(subkey.parent().userID(0))); } else { item->setData(Certificate, Qt::DisplayRole, DN(subkey.parent().userID(0).id()).prettyDN()); } item->setData(Certificate, Qt::ToolTipRole, Formatting::toolTip(subkey.parent(), toolTipOptions())); } } static std::vector<QAction *> actionsForCardSlot(SmartCard::AppType appType) { switch (appType) { case AppType::NetKeyApp: case AppType::P15App: return SmartCardActions::instance()->actions({u"card_all_show_certificate_details"_s}); case AppType::OpenPGPApp: case AppType::PIVApp: case AppType::NoApp: break; }; return {}; } static bool canImportCertificates(const Card *card, const std::vector<std::string> &keyRefsWithoutSMimeCertificate) { switch (card->appType()) { case AppType::OpenPGPApp: // no S/MIME certificates to learn from OpenPGP cards return false; case AppType::NetKeyApp: case AppType::P15App: return !keyRefsWithoutSMimeCertificate.empty(); case AppType::PIVApp: // check whether there are S/MIME certificates for the given card slots return std::ranges::any_of(keyRefsWithoutSMimeCertificate, [card](const auto &keyRef) { return !card->certificateData(keyRef).empty(); }); case AppType::NoApp: break; } return false; } static inline int compareByProtocolAndFingerprint(const Subkey &a, const Subkey &b) { if (a.parent().protocol() < b.parent().protocol()) { return -1; } if (a.parent().protocol() > b.parent().protocol()) { return 1; } return qstrcmp(a.fingerprint(), b.fingerprint()); } static auto getSortedSubkeys(const std::string &keyGrip) { auto subkeys = KeyCache::instance()->findSubkeysByKeyGrip(keyGrip); // sort subkeys by protocol and fingerprint to ensure a stable list order auto lessByProtocolAndFingerprint = [](const Subkey &a, const Subkey &b) { return compareByProtocolAndFingerprint(a, b) < 0; }; std::sort(subkeys.begin(), subkeys.end(), lessByProtocolAndFingerprint); return subkeys; } CardKeysView::CardKeysView(QWidget *parent, Options options) : QWidget{parent} , mOptions{options} { auto mainLayout = new QVBoxLayout{this}; mainLayout->setContentsMargins({}); // The certificate view mTreeWidget = new TreeWidget{this}; mTreeWidget->setAccessibleName(i18nc("@title", "card keys and certificates")); mTreeWidget->setSelectionBehavior(QAbstractItemView::SelectRows); mTreeWidget->setSelectionMode(QAbstractItemView::SingleSelection); mTreeWidget->setRootIsDecorated(false); // set a smaller default column size (default for most styles is 100) so that the Actions column doesn't get too wide by default mTreeWidget->header()->setDefaultSectionSize(20); mTreeWidget->setHeaderLabels({ i18nc("@title:column Key slot of a smart card", "Slot"), i18nc("@title:column", "Keygrip"), i18nc("@title:column", "Usage"), i18nc("@title:column", "Created"), i18nc("@title:column", "Fingerprint"), i18nc("@title:column", "Certificate"), u" "_s, // Actions column shouldn't have a header title; set " " to prevent Qt using the column number }); if (mOptions & NoCreated) { mTreeWidget->forceColumnHidden(Created); } mainLayout->addWidget(mTreeWidget); if (auto action = SmartCardActions::instance()->action(u"card_all_show_certificate_details"_s)) { connect(mTreeWidget, &QAbstractItemView::doubleClicked, action, &QAction::trigger); } mTreeViewOverlay = new ProgressOverlay{mTreeWidget, this}; mTreeViewOverlay->hide(); connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, this, [this]() { updateKeyList(nullptr); }); } CardKeysView::~CardKeysView() = default; void CardKeysView::setCard(const Card *card) { mSerialNumber = card->serialNumber(); mAppName = card->appName(); mAppType = card->appType(); updateKeyList(card); } std::string CardKeysView::currentCardSlot() const { if (const CardKeysWidgetItem *current = static_cast<CardKeysWidgetItem *>(mTreeWidget->currentItem())) { return current->keyRef(); } return {}; } Key CardKeysView::currentCertificate() const { if (const CardKeysWidgetItem *current = static_cast<CardKeysWidgetItem *>(mTreeWidget->currentItem())) { return current->subkey().parent(); } qCDebug(KLEOPATRA_LOG) << __func__ << "- no current item"; return {}; } bool CardKeysView::eventFilter(QObject *obj, QEvent *event) { if ((event->type() == QEvent::FocusOut) // && (obj == mTreeWidget->itemWidget(mTreeWidget->currentItem(), Actions))) { // workaround for missing update when last actions button loses focus mTreeWidget->viewport()->update(); } return QWidget::eventFilter(obj, event); } void CardKeysView::updateKeyList(const Card *card) { qCDebug(KLEOPATRA_LOG) << __func__; const bool firstSetUp = (mTreeWidget->topLevelItemCount() == 0); if (mSerialNumber.empty()) { // ignore KeyCache::keysMayHaveChanged signal until the card has been set return; } const auto cardRefHolder = card ? std::shared_ptr<Card>{} : ReaderStatus::instance()->getCard(mSerialNumber, mAppName); if (!card) { card = cardRefHolder.get(); } if (!card) { qCDebug(KLEOPATRA_LOG) << "Failed to find the" << mAppName << "smart card with the serial number" << mSerialNumber; return; } std::vector<std::string> keyRefsWithoutSMimeCertificate; const auto cardKeyInfos = card->keyInfos(); mCertificates.clear(); mCertificates.reserve(cardKeyInfos.size()); for (int slotIndex = 0; slotIndex < int(cardKeyInfos.size()); ++slotIndex) { const auto &keyInfo = cardKeyInfos[slotIndex]; bool haveFoundSMimeCertificate = false; const auto subkeys = getSortedSubkeys(keyInfo.grip); auto items = getItems(mTreeWidget, slotIndex); if (subkeys.empty()) { if (items.empty()) { Q_ASSERT(firstSetUp); insertTreeWidgetItem(card, slotIndex, keyInfo, Subkey{}); } else { updateTreeWidgetItem(items.front(), keyInfo, Subkey{}, mOptions); for (int i = 1; i < int(items.size()); ++i) { auto item = items.at(i); qCDebug(KLEOPATRA_LOG) << __func__ << "deleting item - slot:" << item->slotIndex() << "certificate:" << item->subkey().parent(); delete item; } } } else { if (items.empty()) { Q_ASSERT(firstSetUp); for (const auto &subkey : subkeys) { insertTreeWidgetItem(card, slotIndex, keyInfo, subkey); } } else if (items.front()->subkey().isNull()) { // the second most simple case: slot with no associated subkeys -> slot with one or more associated subkeys Q_ASSERT(items.size() == 1); updateTreeWidgetItem(items.front(), keyInfo, subkeys.front(), mOptions); const int itemIndex = mTreeWidget->indexOfTopLevelItem(items.front()); for (int i = 1; i < int(subkeys.size()); ++i) { insertTreeWidgetItem(card, slotIndex, keyInfo, subkeys.at(i), itemIndex + i); } } else { // the complicated case; we make use of the known order of the existing items and subkeys int i = 0; int s = 0; while (i < int(items.size()) && s < int(subkeys.size())) { auto item = items.at(i); const Subkey &subkey = subkeys.at(s); const int itemVsSubkey = compareByProtocolAndFingerprint(item->subkey(), subkey); if (itemVsSubkey < 0) { // this subkey is gone qCDebug(KLEOPATRA_LOG) << __func__ << "deleting item - slot:" << item->slotIndex() << "certificate:" << item->subkey().parent(); delete item; ++i; } else if (itemVsSubkey == 0) { updateTreeWidgetItem(item, keyInfo, subkey, mOptions); ++i; ++s; } else { // this subkey is new; insert it before the current item const int itemIndex = mTreeWidget->indexOfTopLevelItem(item); insertTreeWidgetItem(card, slotIndex, keyInfo, subkey, itemIndex); ++s; } } for (; i < int(items.size()); ++i) { auto item = items.at(i); qCDebug(KLEOPATRA_LOG) << __func__ << "deleting item - slot:" << item->slotIndex() << "certificate:" << item->subkey().parent(); delete item; } // insert remaining new subkeys after last item for slotIndex int insertIndex = 0; while ((insertIndex < mTreeWidget->topLevelItemCount()) // && (static_cast<CardKeysWidgetItem *>(mTreeWidget->topLevelItem(insertIndex))->slotIndex() <= slotIndex)) { ++insertIndex; } insertIndex -= s; for (; s < int(subkeys.size()); ++s) { insertTreeWidgetItem(card, slotIndex, keyInfo, subkeys.at(s), insertIndex + s); } } for (const auto &subkey : subkeys) { if (subkey.parent().protocol() == GpgME::CMS) { qCDebug(KLEOPATRA_LOG) << __func__ << "Found S/MIME certificate for card key" << keyInfo.grip << "in cache:" << subkey.parent(); haveFoundSMimeCertificate = true; mCertificates.push_back(subkey.parent()); } } } if (!keyInfo.grip.empty() && !haveFoundSMimeCertificate) { qCDebug(KLEOPATRA_LOG) << __func__ << "Did not find an S/MIME certificates for card key" << keyInfo.grip << "in cache"; keyRefsWithoutSMimeCertificate.push_back(keyInfo.keyRef); } } if (firstSetUp && !mTreeWidget->restoreColumnLayout(u"CardKeysView-"_s + QString::fromStdString(mAppName))) { mTreeWidget->header()->resizeSections(QHeaderView::ResizeToContents); } ensureCertificatesAreValidated(); if (firstSetUp && canImportCertificates(card, keyRefsWithoutSMimeCertificate)) { // the card contains keys we don't know; try to learn them from the card learnCard(); } } void CardKeysView::insertTreeWidgetItem(const Card *card, int slotIndex, const KeyPairInfo &keyInfo, const Subkey &subkey, int index) { qCDebug(KLEOPATRA_LOG) << __func__ << "slot:" << slotIndex << "certificate:" << subkey.parent() << "index:" << index; if (index == -1) { index = mTreeWidget->topLevelItemCount(); } auto item = new CardKeysWidgetItem{slotIndex, keyInfo.keyRef}; item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren); updateTreeWidgetItem(item, keyInfo, subkey, mOptions); mTreeWidget->insertTopLevelItem(index, item); - auto actionsButton = createActionsButton(card->appType()); - mTreeWidget->setItemWidget(item, Actions, actionsButton); + auto actionsButton = addActionsButton(item, card->appType()); if (index == 0) { forceSetTabOrder(mTreeWidget, actionsButton); } else { auto prevActionsButton = mTreeWidget->itemWidget(mTreeWidget->topLevelItem(index - 1), Actions); forceSetTabOrder(prevActionsButton, actionsButton); } - // ensure that current item is set to the right item before the action is triggered; - // interestingly, focus is given to the tree widget instead of the clicked button so that - // the event filtering of QAbstractItemView doesn't take care of this - connect(actionsButton, &QAbstractButton::pressed, mTreeWidget, [this, item]() { - mTreeWidget->setCurrentItem(item, Actions); - }); actionsButton->installEventFilter(this); } -QToolButton *CardKeysView::createActionsButton(SmartCard::AppType appType) +QToolButton *CardKeysView::addActionsButton(QTreeWidgetItem *item, SmartCard::AppType appType) { const auto actions = actionsForCardSlot(appType); auto button = new QToolButton; if (actions.size() == 1) { button->setDefaultAction(actions.front()); + // ensure that current item is set to the right item before the action is triggered; + // interestingly, focus is given to the tree widget instead of the clicked button so that + // the event filtering of QAbstractItemView doesn't take care of this + connect(button, &QAbstractButton::pressed, mTreeWidget, [this, item]() { + mTreeWidget->setCurrentItem(item, Actions); + }); } else { button->setPopupMode(QToolButton::InstantPopup); button->setIcon(QIcon::fromTheme(QStringLiteral("application-menu"))); button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); button->setAccessibleName(i18nc("@action:button", "Actions")); button->setToolTip(i18nc("@info", "Show actions available for this smart card slot")); - auto menu = new QMenu{button}; - for (auto action : actions) { - menu->addAction(action); - } - button->setMenu(menu); + // show the menu *after* the clicked item is set as current item to ensure correct action states + connect(button, &QAbstractButton::pressed, mTreeWidget, [this, item, button, appType]() { + mTreeWidget->setCurrentItem(item, Actions); + QMenu menu{button}; + for (auto action : actionsForCardSlot(appType)) { + menu.addAction(action); + } + button->setMenu(&menu); + button->showMenu(); + button->setMenu(nullptr); + }); } + mTreeWidget->setItemWidget(item, Actions, button); return button; } void CardKeysView::ensureCertificatesAreValidated() { if (mCertificates.empty()) { return; } std::vector<GpgME::Key> certificatesToValidate; certificatesToValidate.reserve(mCertificates.size()); std::ranges::copy_if(mCertificates, std::back_inserter(certificatesToValidate), [this](const auto &cert) { // don't bother validating certificates that have expired or are otherwise invalid return !cert.isBad() && !mValidatedCertificates.contains(cert); }); if (!certificatesToValidate.empty()) { startCertificateValidation(certificatesToValidate); mValidatedCertificates.insert(certificatesToValidate.cbegin(), certificatesToValidate.cend()); } } void CardKeysView::startCertificateValidation(const std::vector<GpgME::Key> &certificates) { qCDebug(KLEOPATRA_LOG) << __func__ << "Validating certificates" << certificates; auto job = std::unique_ptr<QGpgME::KeyListJob>{QGpgME::smime()->keyListJob(false, true, true)}; auto ctx = QGpgME::Job::context(job.get()); ctx->addKeyListMode(GpgME::WithSecret); connect(job.get(), &QGpgME::KeyListJob::result, this, &CardKeysView::certificateValidationDone); job->start(Kleo::getFingerprints(certificates)); job.release(); } void CardKeysView::certificateValidationDone(const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &validatedCertificates) { qCDebug(KLEOPATRA_LOG) << __func__ << "certificates:" << validatedCertificates; if (result.error()) { qCDebug(KLEOPATRA_LOG) << __func__ << "Validating certificates failed:" << result.error(); return; } // replace the current certificates with the validated certificates for (const auto &validatedCert : validatedCertificates) { const auto fpr = validatedCert.primaryFingerprint(); const auto it = std::find_if(mCertificates.begin(), mCertificates.end(), [fpr](const auto &cert) { return !qstrcmp(fpr, cert.primaryFingerprint()); }); if (it != mCertificates.end()) { *it = validatedCert; } else { qCDebug(KLEOPATRA_LOG) << __func__ << "Didn't find validated certificate in certificate list:" << validatedCert; } } updateKeyList(); } void CardKeysView::learnCard() { qCDebug(KLEOPATRA_LOG) << __func__; mTreeViewOverlay->setText(i18nc("@info", "Reading certificates from smart card ...")); mTreeViewOverlay->showOverlay(); ReaderStatus::mutableInstance()->learnCards(GpgME::CMS); connect(ReaderStatus::instance(), &ReaderStatus::cardsLearned, this, [this]() { qCDebug(KLEOPATRA_LOG) << "ReaderStatus::cardsLearned"; mTreeViewOverlay->hideOverlay(); }); } #include "moc_cardkeysview.cpp" diff --git a/src/view/cardkeysview.h b/src/view/cardkeysview.h index 3738e938e..a4122c828 100644 --- a/src/view/cardkeysview.h +++ b/src/view/cardkeysview.h @@ -1,91 +1,92 @@ /* view/cardkeysview.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2024 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include <Libkleo/Predicates> #include <QHash> #include <QWidget> #include <set> #include <string> #include <vector> class QAction; class QToolButton; +class QTreeWidgetItem; namespace GpgME { class Key; class KeyListResult; } namespace Kleo { class ProgressOverlay; class TreeWidget; namespace SmartCard { enum class AppType; class Card; struct KeyPairInfo; } class CardKeysView : public QWidget { Q_OBJECT public: enum Option { // clang-format off ShowSlotName = 0x0001, // show the slot name instead of the slot index NoCreated = 0x0002, // no "Created" column // clang-format on }; Q_DECLARE_FLAGS(Options, Option) explicit CardKeysView(QWidget *parent, Options options); ~CardKeysView() override; void setCard(const SmartCard::Card *card); std::string currentCardSlot() const; GpgME::Key currentCertificate() const; protected: bool eventFilter(QObject *obj, QEvent *event) override; private: void updateKeyList(const SmartCard::Card *card = nullptr); void insertTreeWidgetItem(const SmartCard::Card *card, int slotIndex, const SmartCard::KeyPairInfo &keyInfo, const GpgME::Subkey &subkey, int treeIndex = -1); - QToolButton *createActionsButton(SmartCard::AppType cardType); + QToolButton *addActionsButton(QTreeWidgetItem *item, SmartCard::AppType cardType); void ensureCertificatesAreValidated(); void startCertificateValidation(const std::vector<GpgME::Key> &certificates); void certificateValidationDone(const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys); void learnCard(); private: Options mOptions; std::string mSerialNumber; std::string mAppName; Kleo::SmartCard::AppType mAppType; std::vector<GpgME::Key> mCertificates; // only S/MIME certificates using KeySet = std::set<GpgME::Key, _detail::ByFingerprint<std::less>>; KeySet mValidatedCertificates; TreeWidget *mTreeWidget = nullptr; ProgressOverlay *mTreeViewOverlay = nullptr; }; } // namespace Kleo Q_DECLARE_OPERATORS_FOR_FLAGS(Kleo::CardKeysView::Options)