diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp index 666ad155d..a9bebebf5 100644 --- a/src/dialogs/certificatedetailswidget.cpp +++ b/src/dialogs/certificatedetailswidget.cpp @@ -1,1041 +1,1042 @@ /* dialogs/certificatedetailswidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-FileCopyrightText: 2022 Felix Tiede SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certificatedetailswidget.h" #include "kleopatra_debug.h" #include "exportdialog.h" #include "trustchainwidget.h" #include "subkeyswidget.h" #include "weboftrustdialog.h" #include "commands/changepassphrasecommand.h" #include "commands/changeexpirycommand.h" #include "commands/certifycertificatecommand.h" #ifdef MAILAKONADI_ENABLED #include "commands/exportopenpgpcerttoprovidercommand.h" #endif // MAILAKONADI_ENABLED #include "commands/refreshcertificatecommand.h" #include "commands/revokecertificationcommand.h" #include "commands/revokeuseridcommand.h" #include "commands/adduseridcommand.h" #include "commands/genrevokecommand.h" #include "commands/detailscommand.h" #include "commands/dumpcertificatecommand.h" #include "utils/keys.h" #include "utils/tags.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 Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; static const char *accessibleNameProperty = "_kleo_accessibleName"; class InfoField { public: InfoField(const QString &label, QWidget *parent); void setValue(const QString &value, const QString &accessibleValue = {}); void setAction(const QAction *action); void setToolTip(const QString &toolTip); void setVisible(bool visible); QLabel *label() const { return mLabel; } QLayout *layout() const { return mLayout; } private: void onActionChanged(); QLabel *mLabel = nullptr; QHBoxLayout *mLayout = nullptr; QLabel *mValue = nullptr; QPushButton *mButton = nullptr; const QAction *mAction = nullptr; }; InfoField::InfoField(const QString &label, QWidget *parent) : mLabel{new QLabel{label, parent}} , mLayout{new QHBoxLayout} , mValue{new QLabel{parent}} , mButton{new QPushButton{parent}} { mLabel->setBuddy(mValue); mLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); mValue->setTextInteractionFlags(Qt::TextSelectableByMouse); mValue->setFocusPolicy(Qt::TabFocus); mLayout->addWidget(mValue); mButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); mButton->setVisible(false); mLayout->addWidget(mButton); mLayout->addStretch(); } void InfoField::setValue(const QString &value, const QString &accessibleValue) { mValue->setText(value); mValue->setAccessibleName(accessibleValue); } void InfoField::setAction(const QAction *action) { if (action == mAction) { return; } if (mAction) { QObject::disconnect(mButton, {}, mAction, {}); QObject::disconnect(mAction, {}, mButton, {}); } mAction = action; if (mAction) { QObject::connect(mButton, &QPushButton::clicked, action, &QAction::trigger); QObject::connect(mAction, &QAction::changed, mButton, [this]() { onActionChanged(); }); onActionChanged(); mButton->setAccessibleName(mAction->property(accessibleNameProperty).toString()); mButton->setVisible(true); } else { mButton->setVisible(false); mButton->setText({}); mButton->setIcon({}); } } void InfoField::setToolTip(const QString &toolTip) { mValue->setToolTip(toolTip); } void InfoField::setVisible(bool visible) { mLabel->setVisible(visible); mValue->setVisible(visible); mButton->setVisible(visible && mAction); } void InfoField::onActionChanged() { if (!mAction) { return; } if (mAction->text() != mButton->text()) { mButton->setText(mAction->text()); } mButton->setIcon(mAction->icon()); if (mAction->toolTip() != mButton->toolTip()) { mButton->setToolTip(mAction->toolTip()); } if (mAction->isEnabled() != mButton->isEnabled()) { mButton->setEnabled(mAction->isEnabled()); } } class CertificateDetailsWidget::Private { public: Private(CertificateDetailsWidget *qq); void setupCommonProperties(); void setupPGPProperties(); void setupSMIMEProperties(); void revokeUserID(const GpgME::UserID &uid); void genRevokeCert(); void refreshCertificate(); void certifyClicked(); void webOfTrustClicked(); void exportClicked(); void addUserID(); void changePassphrase(); void changeExpiration(); void keysMayHaveChanged(); void showTrustChainDialog(); void showMoreDetails(); void userIDTableContextMenuRequested(const QPoint &p); QString tofuTooltipString(const GpgME::UserID &uid) const; void showIssuerCertificate(); void updateKey(); void setUpdatedKey(const GpgME::Key &key); void keyListDone(const GpgME::KeyListResult &, const std::vector &, const QString &, const GpgME::Error &); void copyFingerprintToClipboard(); private: CertificateDetailsWidget *const q; public: GpgME::Key key; bool updateInProgress = false; private: struct UI { QLabel *label = nullptr; InfoField *smimeOwnerField = nullptr; QLabel *smimeRelatedAddresses = nullptr; QTreeWidget *userIDTable = nullptr; QGroupBox *groupBox = nullptr; InfoField *validFromField = nullptr; InfoField *expiresField = nullptr; QAction *changeExpirationAction = nullptr; InfoField *typeField = nullptr; InfoField *fingerprintField = nullptr; QAction *copyFingerprintAction = nullptr; InfoField *smimeIssuerField = nullptr; QAction *showIssuerCertificateAction = nullptr; InfoField *complianceField = nullptr; InfoField *trustedIntroducerField = nullptr; QPushButton *addUserIDBtn = nullptr; QPushButton *changePassphraseBtn = nullptr; QPushButton *trustChainDetailsBtn = nullptr; QPushButton *genRevokeBtn = nullptr; QPushButton *refreshBtn = nullptr; QPushButton *certifyBtn = nullptr; QPushButton *moreDetailsBtn = nullptr; QPushButton *exportBtn = nullptr; QPushButton *webOfTrustBtn = nullptr; void setupUi(QWidget *parent) { auto mainLayout = new QVBoxLayout{parent}; label = new QLabel(i18n("You can use this certificate to secure communication with the following email addresses:"), parent); label->setWordWrap(true); mainLayout->addWidget(label); { auto gridLayout_2 = new QGridLayout; gridLayout_2->setColumnStretch(1, 1); int row = 0; smimeOwnerField = new InfoField{i18n("Owner:"), parent}; gridLayout_2->addWidget(smimeOwnerField->label(), row, 0, 1, 1); gridLayout_2->addLayout(smimeOwnerField->layout(), row, 1, 1, 1); row++; smimeRelatedAddresses = new QLabel(i18n("Related addresses:"), parent); QFont font; font.setBold(true); font.setWeight(75); smimeRelatedAddresses->setFont(font); gridLayout_2->addWidget(smimeRelatedAddresses, row, 0, 1, 1); mainLayout->addLayout(gridLayout_2); } userIDTable = new QTreeWidget(parent); QTreeWidgetItem *__qtreewidgetitem = new QTreeWidgetItem(); __qtreewidgetitem->setText(0, QString::fromUtf8("1")); userIDTable->setHeaderItem(__qtreewidgetitem); userIDTable->setEditTriggers(QAbstractItemView::NoEditTriggers); userIDTable->setSelectionMode(QAbstractItemView::SingleSelection); userIDTable->setRootIsDecorated(false); userIDTable->setUniformRowHeights(true); userIDTable->setAllColumnsShowFocus(true); mainLayout->addWidget(userIDTable); { auto hboxLayout_1 = new QHBoxLayout; addUserIDBtn = new QPushButton(i18nc("@action:button", "Add User ID"), parent); hboxLayout_1->addWidget(addUserIDBtn); changePassphraseBtn = new QPushButton(i18nc("@action:button", "Change Passphrase"), parent); hboxLayout_1->addWidget(changePassphraseBtn); trustChainDetailsBtn = new QPushButton(i18nc("@action:button", "Trust Chain Details..."), parent); hboxLayout_1->addWidget(trustChainDetailsBtn); genRevokeBtn = new QPushButton(i18nc("@action:button", "Generate Revocation Certificate"), parent); genRevokeBtn->setToolTip(u"" % i18n("A revocation certificate is a file that serves as a \"kill switch\" to publicly " "declare that a key shall not anymore be used. It is not possible " "to retract such a revocation certificate once it has been published.") % u""); hboxLayout_1->addWidget(genRevokeBtn); refreshBtn = new QPushButton{i18nc("@action:button", "Update"), parent}; #ifndef QGPGME_SUPPORTS_KEY_REFRESH refreshBtn->setVisible(false); #endif hboxLayout_1->addWidget(refreshBtn); certifyBtn = new QPushButton(i18nc("@action:button", "Certify"), parent); hboxLayout_1->addWidget(certifyBtn); hboxLayout_1->addStretch(1); mainLayout->addLayout(hboxLayout_1); } groupBox = new QGroupBox(i18n("Certificate Details"), parent); groupBox->setFlat(false); { auto gridLayout = new QGridLayout(groupBox); gridLayout->setColumnStretch(1, 1); int boxRow = 0; validFromField = new InfoField{i18n("Valid from:"), groupBox}; gridLayout->addWidget(validFromField->label(), boxRow, 0, 1, 1); gridLayout->addLayout(validFromField->layout(), boxRow, 1, 1, 1); boxRow++; expiresField = new InfoField{i18n("Expires:"), groupBox}; changeExpirationAction = new QAction{parent}; changeExpirationAction->setIcon(QIcon::fromTheme(QStringLiteral("editor"))); changeExpirationAction->setToolTip(i18nc("@info:tooltip", "Change the expiration date")); changeExpirationAction->setProperty(accessibleNameProperty, i18nc("@action:button", "Change expiration")); expiresField->setAction(changeExpirationAction); gridLayout->addWidget(expiresField->label(), boxRow, 0, 1, 1); gridLayout->addLayout(expiresField->layout(), boxRow, 1, 1, 1); boxRow++; typeField = new InfoField{i18n("Type:"), groupBox}; gridLayout->addWidget(typeField->label(), boxRow, 0, 1, 1); gridLayout->addLayout(typeField->layout(), boxRow, 1, 1, 1); boxRow++; fingerprintField = new InfoField{i18n("Fingerprint:"), groupBox}; if (QGuiApplication::clipboard()) { copyFingerprintAction = new QAction{parent}; copyFingerprintAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); copyFingerprintAction->setToolTip(i18nc("@info:tooltip", "Copy the fingerprint to the clipboard")); copyFingerprintAction->setProperty(accessibleNameProperty, i18nc("@action:button", "Copy fingerprint")); fingerprintField->setAction(copyFingerprintAction); } gridLayout->addWidget(fingerprintField->label(), boxRow, 0, 1, 1); gridLayout->addLayout(fingerprintField->layout(), boxRow, 1, 1, 1); boxRow++; smimeIssuerField = new InfoField{i18n("Issuer:"), groupBox}; showIssuerCertificateAction = new QAction{parent}; showIssuerCertificateAction->setIcon(QIcon::fromTheme(QStringLiteral("dialog-information"))); showIssuerCertificateAction->setToolTip(i18nc("@info:tooltip", "Show the issuer certificate")); showIssuerCertificateAction->setProperty(accessibleNameProperty, i18nc("@action:button", "Show certificate")); smimeIssuerField->setAction(showIssuerCertificateAction); gridLayout->addWidget(smimeIssuerField->label(), boxRow, 0, 1, 1); gridLayout->addLayout(smimeIssuerField->layout(), boxRow, 1, 1, 1); boxRow++; complianceField = new InfoField{i18n("Compliance:"), groupBox}; gridLayout->addWidget(complianceField->label(), boxRow, 0, 1, 1); gridLayout->addLayout(complianceField->layout(), boxRow, 1, 1, 1); boxRow++; trustedIntroducerField = new InfoField{i18n("Trusted introducer for:"), groupBox}; gridLayout->addWidget(trustedIntroducerField->label(), boxRow, 0, 1, 1); trustedIntroducerField->setToolTip(i18n("See certifications for details.")); gridLayout->addLayout(trustedIntroducerField->layout(), boxRow, 1, 1, 1); boxRow++; auto horizontalLayout = new QHBoxLayout; moreDetailsBtn = new QPushButton(i18nc("@action:button", "More Details..."), groupBox); horizontalLayout->addWidget(moreDetailsBtn); exportBtn = new QPushButton(i18nc("@action:button", "Export..."), groupBox); horizontalLayout->addWidget(exportBtn); webOfTrustBtn = new QPushButton(i18nc("@action:button", "Certifications..."), groupBox); horizontalLayout->addWidget(webOfTrustBtn); horizontalLayout->addStretch(1); gridLayout->addLayout(horizontalLayout, boxRow, 0, 1, 2); } mainLayout->addWidget(groupBox); } } ui; }; CertificateDetailsWidget::Private::Private(CertificateDetailsWidget *qq) : q{qq} { ui.setupUi(q); connect(ui.userIDTable, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) { userIDTableContextMenuRequested(p); }); connect(ui.addUserIDBtn, &QPushButton::clicked, q, [this]() { addUserID(); }); connect(ui.changePassphraseBtn, &QPushButton::clicked, q, [this]() { changePassphrase(); }); connect(ui.genRevokeBtn, &QPushButton::clicked, q, [this]() { genRevokeCert(); }); connect(ui.changeExpirationAction, &QAction::triggered, q, [this]() { changeExpiration(); }); connect(ui.showIssuerCertificateAction, &QAction::triggered, q, [this]() { showIssuerCertificate(); }); connect(ui.trustChainDetailsBtn, &QPushButton::pressed, q, [this]() { showTrustChainDialog(); }); connect(ui.moreDetailsBtn, &QPushButton::pressed, q, [this]() { showMoreDetails(); }); connect(ui.refreshBtn, &QPushButton::clicked, q, [this]() { refreshCertificate(); }); connect(ui.certifyBtn, &QPushButton::clicked, q, [this]() { certifyClicked(); }); connect(ui.webOfTrustBtn, &QPushButton::clicked, q, [this]() { webOfTrustClicked(); }); connect(ui.exportBtn, &QPushButton::clicked, q, [this]() { exportClicked(); }); if (ui.copyFingerprintAction) { connect(ui.copyFingerprintAction, &QAction::triggered, q, [this]() { copyFingerprintToClipboard(); }); } connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { keysMayHaveChanged(); }); } void CertificateDetailsWidget::Private::setupCommonProperties() { const bool hasSecret = key.hasSecret(); const bool isOpenPGP = key.protocol() == GpgME::OpenPGP; ui.changePassphraseBtn->setVisible(hasSecret); ui.genRevokeBtn->setVisible(isOpenPGP && hasSecret); ui.certifyBtn->setVisible(isOpenPGP && !hasSecret); if (isOpenPGP && hasSecret) { ui.expiresField->setAction(ui.changeExpirationAction); } else { ui.expiresField->setAction(nullptr); } ui.addUserIDBtn->setVisible(hasSecret && isOpenPGP); ui.webOfTrustBtn->setVisible(isOpenPGP); ui.validFromField->setValue(Formatting::creationDateString(key), Formatting::accessibleCreationDate(key)); ui.expiresField->setValue(Formatting::expirationDateString(key, i18nc("Expires", "never")), Formatting::accessibleExpirationDate(key, i18nc("Expires", "never"))); ui.typeField->setValue(Kleo::Formatting::type(key)); - ui.fingerprintField->setValue(Formatting::prettyID(key.primaryFingerprint())); + ui.fingerprintField->setValue(Formatting::prettyID(key.primaryFingerprint()), + Formatting::accessibleHexID(key.primaryFingerprint())); ui.complianceField->setVisible(Kleo::gnupgIsDeVsCompliant()); if (Kleo::gnupgIsDeVsCompliant()) { ui.complianceField->setValue(Kleo::Formatting::complianceStringForKey(key)); } ui.userIDTable->clear(); QStringList headers = { i18n("Email"), i18n("Name"), i18n("Trust Level"), i18n("Tags") }; ui.userIDTable->setColumnCount(headers.count()); ui.userIDTable->setColumnWidth(0, 200); ui.userIDTable->setColumnWidth(1, 200); ui.userIDTable->setHeaderLabels(headers); const auto uids = key.userIDs(); for (unsigned int i = 0; i < uids.size(); ++i) { const auto &uid = uids[i]; auto item = new QTreeWidgetItem; const QString toolTip = tofuTooltipString(uid); item->setData(0, Qt::UserRole, QVariant::fromValue(uid)); auto pMail = Kleo::Formatting::prettyEMail(uid); auto pName = Kleo::Formatting::prettyName(uid); if (!isOpenPGP && pMail.isEmpty() && !pName.isEmpty()) { // S/MIME UserIDs are sometimes split, with one userID // containing the name another the Mail, we merge these // UID's into a single item. if (i + 1 < uids.size()) { pMail = Kleo::Formatting::prettyEMail(uids[i + 1]); // skip next uid ++i; } } if (!isOpenPGP && pMail.isEmpty() && pName.isEmpty()) { // S/MIME certificates sometimes contain urls where both // name and mail is empty. In that case we print whatever // the uid is as name. // // Can be ugly like (3:uri24:http://ca.intevation.org), but // this is better then showing an empty entry. pName = QString::fromLatin1(uid.id()); } item->setData(0, Qt::DisplayRole, pMail); item->setData(0, Qt::ToolTipRole, toolTip); item->setData(1, Qt::DisplayRole, pName); item->setData(1, Qt::ToolTipRole, toolTip); QIcon trustIcon; if (updateInProgress) { trustIcon = QIcon::fromTheme(QStringLiteral("emblem-question")); item->setData(2, Qt::DisplayRole, i18n("Updating...")); } else { switch (uid.validity()) { case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: trustIcon = QIcon::fromTheme(QStringLiteral("emblem-question")); break; case GpgME::UserID::Never: trustIcon = QIcon::fromTheme(QStringLiteral("emblem-error")); break; case GpgME::UserID::Marginal: trustIcon = QIcon::fromTheme(QStringLiteral("emblem-warning")); break; case GpgME::UserID::Full: case GpgME::UserID::Ultimate: trustIcon = QIcon::fromTheme(QStringLiteral("emblem-success")); break; } item->setData(2, Qt::DisplayRole, Kleo::Formatting::validityShort(uid)); } item->setData(2, Qt::DecorationRole, trustIcon); item->setData(2, Qt::ToolTipRole, toolTip); GpgME::Error err; QStringList tagList; for (const auto &tag: uid.remarks(Tags::tagKeys(), err)) { if (err) { qCWarning(KLEOPATRA_LOG) << "Getting remarks for user ID" << uid.id() << "failed:" << err; } tagList << QString::fromStdString(tag); } qCDebug(KLEOPATRA_LOG) << "tagList:" << tagList; const auto tags = tagList.join(QStringLiteral("; ")); item->setData(3, Qt::DisplayRole, tags); item->setData(3, Qt::ToolTipRole, toolTip); ui.userIDTable->addTopLevelItem(item); } if (!Tags::tagsEnabled()) { ui.userIDTable->hideColumn(3); } } void CertificateDetailsWidget::Private::revokeUserID(const GpgME::UserID &userId) { const QString message = xi18nc( "@info", "Do you really want to revoke the user ID%1 ?", QString::fromUtf8(userId.id())); auto confirmButton = KStandardGuiItem::yes(); confirmButton.setText(i18nc("@action:button", "Revoke User ID")); confirmButton.setToolTip({}); const auto choice = KMessageBox::questionYesNo( q->window(), message, i18nc("@title:window", "Confirm Revocation"), confirmButton, KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::WindowModal); if (choice != KMessageBox::Yes) { return; } auto cmd = new Commands::RevokeUserIDCommand(userId); cmd->setParentWidget(q); ui.userIDTable->setEnabled(false); connect(cmd, &Command::finished, q, [this]() { ui.userIDTable->setEnabled(true); updateKey(); }); cmd->start(); } void CertificateDetailsWidget::Private::changeExpiration() { auto cmd = new Kleo::Commands::ChangeExpiryCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangeExpiryCommand::finished, q, [this]() { ui.changeExpirationAction->setEnabled(true); }); ui.changeExpirationAction->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::changePassphrase() { auto cmd = new Kleo::Commands::ChangePassphraseCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangePassphraseCommand::finished, q, [this]() { ui.changePassphraseBtn->setEnabled(true); }); ui.changePassphraseBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::genRevokeCert() { auto cmd = new Kleo::Commands::GenRevokeCommand(key); QObject::connect(cmd, &Kleo::Commands::GenRevokeCommand::finished, q, [this]() { ui.genRevokeBtn->setEnabled(true); }); ui.genRevokeBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::refreshCertificate() { auto cmd = new Kleo::RefreshCertificateCommand{key}; QObject::connect(cmd, &Kleo::RefreshCertificateCommand::finished, q, [this]() { ui.refreshBtn->setEnabled(true); }); ui.refreshBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::certifyClicked() { auto cmd = new Kleo::Commands::CertifyCertificateCommand(key); QObject::connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this]() { ui.certifyBtn->setEnabled(true); }); ui.certifyBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::webOfTrustClicked() { QScopedPointer dlg(new WebOfTrustDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::exportClicked() { QScopedPointer dlg(new ExportDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::addUserID() { auto cmd = new Kleo::Commands::AddUserIDCommand(key); QObject::connect(cmd, &Kleo::Commands::AddUserIDCommand::finished, q, [this]() { ui.addUserIDBtn->setEnabled(true); updateKey(); }); ui.addUserIDBtn->setEnabled(false); cmd->start(); } namespace { void ensureThatKeyDetailsAreLoaded(GpgME::Key &key) { if (key.userID(0).numSignatures() == 0) { key.update(); } } } void CertificateDetailsWidget::Private::keysMayHaveChanged() { auto newKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint()); if (!newKey.isNull()) { ensureThatKeyDetailsAreLoaded(newKey); setUpdatedKey(newKey); } } void CertificateDetailsWidget::Private::showTrustChainDialog() { QScopedPointer dlg(new TrustChainDialog(q)); dlg->setKey(key); dlg->exec(); } namespace { bool isLastValidUserID(const GpgME::UserID &userId) { if (isRevokedOrExpired(userId)) { return false; } const auto userIds = userId.parent().userIDs(); const int numberOfValidUserIds = std::count_if(std::begin(userIds), std::end(userIds), [](const auto &u) { return !isRevokedOrExpired(u); }); return numberOfValidUserIds == 1; } bool canRevokeUserID(const GpgME::UserID &userId) { const auto key = userId.parent(); return (!userId.isNull() // && key.protocol() == GpgME::OpenPGP && canCreateCertifications(key) && !isLastValidUserID(userId)); } } void CertificateDetailsWidget::Private::userIDTableContextMenuRequested(const QPoint &p) { auto item = ui.userIDTable->itemAt(p); if (!item) { return; } const auto userID = item->data(0, Qt::UserRole).value(); auto menu = new QMenu(q); menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-sign")), i18n("Certify..."), q, [this, userID]() { auto cmd = new Kleo::Commands::CertifyCertificateCommand(userID); ui.userIDTable->setEnabled(false); connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); updateKey(); }); cmd->start(); }); { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), i18nc("@action:inmenu", "Revoke User ID"), q, [this, userID]() { revokeUserID(userID); }); action->setEnabled(canRevokeUserID(userID)); } if (Kleo::Commands::RevokeCertificationCommand::isSupported()) { menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-revoke")), i18n("Revoke Certification..."), q, [this, userID]() { auto cmd = new Kleo::Commands::RevokeCertificationCommand(userID); ui.userIDTable->setEnabled(false); connect(cmd, &Kleo::Commands::RevokeCertificationCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); updateKey(); }); cmd->start(); }); } #ifdef MAILAKONADI_ENABLED if (key.hasSecret() && key.protocol() == GpgME::OpenPGP) { menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18nc("@action:inmenu", "Publish at Mail Provider ..."), q, [this, userID]() { auto cmd = new Kleo::Commands::ExportOpenPGPCertToProviderCommand(userID); ui.userIDTable->setEnabled(false); connect(cmd, &Kleo::Commands::ExportOpenPGPCertToProviderCommand::finished, q, [this]() { ui.userIDTable->setEnabled(true); }); cmd->start(); }); } #endif // MAILAKONADI_ENABLED connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater); menu->popup(ui.userIDTable->viewport()->mapToGlobal(p)); } void CertificateDetailsWidget::Private::showMoreDetails() { ui.moreDetailsBtn->setEnabled(false); if (key.protocol() == GpgME::CMS) { auto cmd = new Kleo::Commands::DumpCertificateCommand(key); connect(cmd, &Kleo::Commands::DumpCertificateCommand::finished, q, [this]() { ui.moreDetailsBtn->setEnabled(true); }); cmd->setUseDialog(true); cmd->start(); } else { QScopedPointer dlg(new SubKeysDialog(q)); dlg->setKey(key); dlg->exec(); ui.moreDetailsBtn->setEnabled(true); } } QString CertificateDetailsWidget::Private::tofuTooltipString(const GpgME::UserID &uid) const { const auto tofu = uid.tofuInfo(); if (tofu.isNull()) { return QString(); } QString html = QStringLiteral(""); const auto appendRow = [&html](const QString &lbl, const QString &val) { html += QStringLiteral("" "" "" "") .arg(lbl, val); }; const auto appendHeader = [this, &html](const QString &hdr) { html += QStringLiteral("") .arg(q->palette().highlight().color().name(), q->palette().highlightedText().color().name(), hdr); }; const auto dateTime = [](long ts) { QLocale l; return ts == 0 ? i18n("never") : l.toString(QDateTime::fromSecsSinceEpoch(ts), QLocale::ShortFormat); }; appendHeader(i18n("Signing")); appendRow(i18n("First message"), dateTime(tofu.signFirst())); appendRow(i18n("Last message"), dateTime(tofu.signLast())); appendRow(i18n("Message count"), QString::number(tofu.signCount())); appendHeader(i18n("Encryption")); appendRow(i18n("First message"), dateTime(tofu.encrFirst())); appendRow(i18n("Last message"), dateTime(tofu.encrLast())); appendRow(i18n("Message count"), QString::number(tofu.encrCount())); html += QStringLiteral("
%1:%2
%3
"); // Make sure the tooltip string is different for each UserID, even if the // data are the same, otherwise the tooltip is not updated and moved when // user moves mouse from one row to another. html += QStringLiteral("").arg(QString::fromUtf8(uid.id())); return html; } namespace { auto isGood(const GpgME::UserID::Signature &signature) { return signature.status() == GpgME::UserID::Signature::NoError && !signature.isInvalid() && 0x10 <= signature.certClass() && signature.certClass() <= 0x13; } auto accumulateTrustDomains(const std::vector &signatures) { return std::accumulate( std::begin(signatures), std::end(signatures), std::set(), [] (auto domains, const auto &signature) { if (isGood(signature) && signature.isTrustSignature()) { domains.insert(Formatting::trustSignatureDomain(signature)); } return domains; } ); } auto accumulateTrustDomains(const std::vector &userIds) { return std::accumulate( std::begin(userIds), std::end(userIds), std::set(), [] (auto domains, const auto &userID) { const auto newDomains = accumulateTrustDomains(userID.signatures()); std::copy(std::begin(newDomains), std::end(newDomains), std::inserter(domains, std::end(domains))); return domains; } ); } } void CertificateDetailsWidget::Private::setupPGPProperties() { ui.smimeOwnerField->setVisible(false); ui.smimeIssuerField->setVisible(false); ui.smimeRelatedAddresses->setVisible(false); ui.trustChainDetailsBtn->setVisible(false); ui.userIDTable->setContextMenuPolicy(Qt::CustomContextMenu); const auto trustDomains = accumulateTrustDomains(key.userIDs()); ui.trustedIntroducerField->setVisible(!trustDomains.empty()); ui.trustedIntroducerField->setValue(QStringList(std::begin(trustDomains), std::end(trustDomains)).join(u", ")); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the key from external sources.")); } static QString formatDNToolTip(const Kleo::DN &dn) { QString html = QStringLiteral(""); const auto appendRow = [&html, dn](const QString &lbl, const QString &attr) { const QString val = dn[attr]; if (!val.isEmpty()) { html += QStringLiteral( "" "" "").arg(lbl, val); } }; appendRow(i18n("Common Name"), QStringLiteral("CN")); appendRow(i18n("Organization"), QStringLiteral("O")); appendRow(i18n("Street"), QStringLiteral("STREET")); appendRow(i18n("City"), QStringLiteral("L")); appendRow(i18n("State"), QStringLiteral("ST")); appendRow(i18n("Country"), QStringLiteral("C")); html += QStringLiteral("
%1:%2
"); return html; } void CertificateDetailsWidget::Private::setupSMIMEProperties() { ui.trustedIntroducerField->setVisible(false); const auto ownerId = key.userID(0); const Kleo::DN dn(ownerId.id()); const QString cn = dn[QStringLiteral("CN")]; const QString o = dn[QStringLiteral("O")]; const QString dnEmail = dn[QStringLiteral("EMAIL")]; const QString name = cn.isEmpty() ? dnEmail : cn; QString owner; if (name.isEmpty()) { owner = dn.dn(); } else if (o.isEmpty()) { owner = name; } else { owner = i18nc(" of ", "%1 of %2", name, o); } ui.smimeOwnerField->setValue(owner); const Kleo::DN issuerDN(key.issuerName()); const QString issuerCN = issuerDN[QStringLiteral("CN")]; const QString issuer = issuerCN.isEmpty() ? QString::fromUtf8(key.issuerName()) : issuerCN; ui.smimeIssuerField->setValue(issuer); ui.smimeIssuerField->setToolTip(formatDNToolTip(issuerDN)); ui.smimeOwnerField->setToolTip(formatDNToolTip(dn)); ui.refreshBtn->setToolTip(i18nc("@info:tooltip", "Update the CRLs and do a full validation check of the certificate.")); } void CertificateDetailsWidget::Private::showIssuerCertificate() { const auto parentKey = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); if (!parentKey.size()) { return; } auto cmd = new Kleo::Commands::DetailsCommand(parentKey[0], nullptr); cmd->setParentWidget(q); cmd->start(); } void CertificateDetailsWidget::Private::copyFingerprintToClipboard() { if (auto clipboard = QGuiApplication::clipboard()) { clipboard->setText(QString::fromLatin1(key.primaryFingerprint())); } } CertificateDetailsWidget::CertificateDetailsWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } CertificateDetailsWidget::~CertificateDetailsWidget() = default; void CertificateDetailsWidget::Private::keyListDone(const GpgME::KeyListResult &, const std::vector &keys, const QString &, const GpgME::Error &) { updateInProgress = false; if (keys.size() != 1) { qCWarning(KLEOPATRA_LOG) << "Invalid keylist result in update."; return; } // As we listen for keysmayhavechanged we get the update // after updating the keycache. KeyCache::mutableInstance()->insert(keys); } void CertificateDetailsWidget::Private::updateKey() { key.update(); setUpdatedKey(key); } void CertificateDetailsWidget::Private::setUpdatedKey(const GpgME::Key &k) { key = k; setupCommonProperties(); if (key.protocol() == GpgME::OpenPGP) { setupPGPProperties(); } else { setupSMIMEProperties(); } } void CertificateDetailsWidget::setKey(const GpgME::Key &key) { if (key.protocol() == GpgME::CMS) { // For everything but S/MIME this should be quick // and we don't need to show another status. d->updateInProgress = true; } d->setUpdatedKey(key); // Run a keylistjob with full details (TOFU / Validate) QGpgME::KeyListJob *job = key.protocol() == GpgME::OpenPGP ? QGpgME::openpgp()->keyListJob(false, true, true) : QGpgME::smime()->keyListJob(false, true, true); auto ctx = QGpgME::Job::context(job); ctx->addKeyListMode(GpgME::WithTofu); ctx->addKeyListMode(GpgME::SignatureNotations); if (key.hasSecret()) { ctx->addKeyListMode(GpgME::WithSecret); } // Windows QGpgME new style connect problem makes this necessary. connect(job, SIGNAL(result(GpgME::KeyListResult,std::vector,QString,GpgME::Error)), this, SLOT(keyListDone(GpgME::KeyListResult,std::vector,QString,GpgME::Error))); job->start(QStringList() << QLatin1String(key.primaryFingerprint())); } GpgME::Key CertificateDetailsWidget::key() const { return d->key; } #include "moc_certificatedetailswidget.cpp"