diff --git a/src/dialogs/certificatedetailswidget.cpp b/src/dialogs/certificatedetailswidget.cpp index 0c7fe8a88..48f6807ea 100644 --- a/src/dialogs/certificatedetailswidget.cpp +++ b/src/dialogs/certificatedetailswidget.cpp @@ -1,568 +1,604 @@ /* Copyright (c) 2016 Klarälvdalens Datakonsult AB 2017 Intevation GmbH Kleopatra is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Kleopatra is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "certificatedetailswidget.h" #include "ui_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" #include "commands/adduseridcommand.h" #include "commands/genrevokecommand.h" #include "commands/detailscommand.h" #include "commands/dumpcertificatecommand.h" #include #include #include +#include #include +#include #include +#include +#include + #include #include #include #include #include #include #define HIDE_ROW(row) \ ui.row->setVisible(false); \ ui.row##Lbl->setVisible(false); Q_DECLARE_METATYPE(GpgME::UserID) using namespace Kleo; class CertificateDetailsWidget::Private { public: Private(CertificateDetailsWidget *parent) : q(parent) {} void setupCommonProperties(); void setupPGPProperties(); void setupSMIMEProperties(); void revokeUID(const GpgME::UserID &uid); void genRevokeCert(); void certifyClicked(); void webOfTrustClicked(); void exportClicked(); void addUserID(); void changePassphrase(); void changeExpiration(); void keysMayHaveChanged(); void showTrustChainDialog(); void showMoreDetails(); void publishCertificate(); void userIDTableContextMenuRequested(const QPoint &p); QString tofuTooltipString(const GpgME::UserID &uid) const; void smimeLinkActivated(const QString &link); + void setUpdatedKey(const GpgME::Key &key); + Ui::CertificateDetailsWidget ui; GpgME::Key key; + bool updateInProgress; private: CertificateDetailsWidget *q; }; void CertificateDetailsWidget::Private::setupCommonProperties() { // TODO: Enable once implemented HIDE_ROW(publishing) const bool hasSecret = key.hasSecret(); const bool isOpenPGP = key.protocol() == GpgME::OpenPGP; // TODO: Enable once implemented const bool canRevokeUID = false; // isOpenPGP && hasSecret ui.changePassphraseBtn->setVisible(hasSecret); ui.genRevokeBtn->setVisible(isOpenPGP && hasSecret); ui.certifyBtn->setVisible(isOpenPGP && !hasSecret); ui.changeExpirationBtn->setVisible(isOpenPGP && hasSecret); ui.addUserIDBtn->setVisible(hasSecret); ui.webOfTrustBtn->setVisible(isOpenPGP); ui.hboxLayout_1->addStretch(1); ui.validFrom->setText(Kleo::Formatting::creationDateString(key)); const QString expiry = Kleo::Formatting::expirationDateString(key); ui.expires->setText(expiry.isEmpty() ? i18nc("Expires", "never") : expiry); ui.type->setText(Kleo::Formatting::type(key)); ui.fingerprint->setText(QString::fromLatin1(key.primaryFingerprint())); if (Kleo::Formatting::complianceMode().isEmpty()) { HIDE_ROW(compliance) } else { ui.complianceLbl->setText(Kleo::Formatting::complianceStringForKey(key)); } ui.userIDTable->clear(); - QStringList headers = { i18n("Email"), i18n("Name") }; - if (isOpenPGP) { - headers << i18n("Trust Level"); - if (canRevokeUID) { - headers << QString(); - } + QStringList headers = { i18n("Email"), i18n("Name"), i18n("Trust Level") }; + if (canRevokeUID) { + headers << QString(); } 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); if (!isOpenPGP && pMail.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; } } item->setData(0, Qt::DisplayRole, pMail); item->setData(0, Qt::ToolTipRole, toolTip); item->setData(1, Qt::DisplayRole, Kleo::Formatting::prettyName(uid)); item->setData(1, Qt::ToolTipRole, toolTip); - if (isOpenPGP) { - QIcon trustIcon; + + 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::DecorationRole, trustIcon); item->setData(2, Qt::DisplayRole, Kleo::Formatting::validityShort(uid)); - item->setData(2, Qt::ToolTipRole, toolTip); - - ui.userIDTable->addTopLevelItem(item); - - if (canRevokeUID) { - auto button = new QPushButton; - button->setIcon(QIcon::fromTheme(QStringLiteral("entry-delete"))); - button->setToolTip(i18n("Revoke this User ID")); - button->setMaximumWidth(32); - QObject::connect(button, &QPushButton::clicked, - q, [this, uid]() { revokeUID(uid); }); - ui.userIDTable->setItemWidget(item, 4, button); - } - } else { - ui.userIDTable->addTopLevelItem(item); + } + item->setData(2, Qt::DecorationRole, trustIcon); + item->setData(2, Qt::ToolTipRole, toolTip); + + ui.userIDTable->addTopLevelItem(item); + + if (canRevokeUID) { + auto button = new QPushButton; + button->setIcon(QIcon::fromTheme(QStringLiteral("entry-delete"))); + button->setToolTip(i18n("Revoke this User ID")); + button->setMaximumWidth(32); + QObject::connect(button, &QPushButton::clicked, + q, [this, uid]() { revokeUID(uid); }); + ui.userIDTable->setItemWidget(item, 4, button); } } } void CertificateDetailsWidget::Private::revokeUID(const GpgME::UserID &uid) { Q_UNUSED(uid); qCWarning(KLEOPATRA_LOG) << "Revoking UserID is not implemented. How did you even get here?!?!"; } void CertificateDetailsWidget::Private::changeExpiration() { auto cmd = new Kleo::Commands::ChangeExpiryCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangeExpiryCommand::finished, q, [this]() { ui.changeExpirationBtn->setEnabled(true); }); ui.changeExpirationBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::changePassphrase() { auto cmd = new Kleo::Commands::ChangePassphraseCommand(key); QObject::connect(cmd, &Kleo::Commands::ChangePassphraseCommand::finished, q, [this, cmd]() { 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, cmd]() { ui.genRevokeBtn->setEnabled(true); }); ui.genRevokeBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::certifyClicked() { auto cmd = new Kleo::Commands::CertifyCertificateCommand(key); QObject::connect(cmd, &Kleo::Commands::CertifyCertificateCommand::finished, q, [this, cmd]() { 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, cmd]() { ui.addUserIDBtn->setEnabled(true); }); ui.addUserIDBtn->setEnabled(false); cmd->start(); } void CertificateDetailsWidget::Private::keysMayHaveChanged() { auto newKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint()); if (!newKey.isNull()) { - q->setKey(newKey); + setUpdatedKey(newKey); } } void CertificateDetailsWidget::Private::showTrustChainDialog() { QScopedPointer dlg(new TrustChainDialog(q)); dlg->setKey(key); dlg->exec(); } void CertificateDetailsWidget::Private::publishCertificate() { qCWarning(KLEOPATRA_LOG) << "publishCertificateis not implemented."; //TODO } 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(); QMenu *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); }); cmd->start(); }); 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) { return ts == 0 ? i18n("never") : QDateTime::fromTime_t(ts).toString(Qt::SystemLocaleShortDate); }; 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; } void CertificateDetailsWidget::Private::setupPGPProperties() { HIDE_ROW(smimeOwner) HIDE_ROW(smimeIssuer) ui.smimeRelatedAddresses->setVisible(false); ui.trustChainDetailsBtn->setVisible(false); ui.userIDTable->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui.userIDTable, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) { userIDTableContextMenuRequested(p); }); } 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() { HIDE_ROW(publishing) 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.smimeOwner->setText(owner); ui.smimeOwner->setTextInteractionFlags(Qt::TextBrowserInteraction); const Kleo::DN issuerDN(key.issuerName()); const QString issuerCN = issuerDN[QStringLiteral("CN")]; const QString issuer = issuerCN.isEmpty() ? key.issuerName() : issuerCN; ui.smimeIssuer->setText(QStringLiteral("%1").arg(issuer)); ui.smimeIssuer->setToolTip(formatDNToolTip(issuerDN)); ui.smimeOwner->setToolTip(formatDNToolTip(dn)); } void CertificateDetailsWidget::Private::smimeLinkActivated(const QString &link) { if (link == QLatin1String("#issuerDetails")) { 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(); return; } qCWarning(KLEOPATRA_LOG) << "Unknown link activated:" << link; } CertificateDetailsWidget::CertificateDetailsWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { d->ui.setupUi(this); connect(d->ui.addUserIDBtn, &QPushButton::clicked, this, [this]() { d->addUserID(); }); connect(d->ui.changePassphraseBtn, &QPushButton::clicked, this, [this]() { d->changePassphrase(); }); connect(d->ui.genRevokeBtn, &QPushButton::clicked, this, [this]() { d->genRevokeCert(); }); connect(d->ui.changeExpirationBtn, &QPushButton::clicked, this, [this]() { d->changeExpiration(); }); connect(d->ui.smimeOwner, &QLabel::linkActivated, this, [this](const QString &link) { d->smimeLinkActivated(link); }); connect(d->ui.smimeIssuer, &QLabel::linkActivated, this, [this](const QString &link) { d->smimeLinkActivated(link); }); connect(d->ui.trustChainDetailsBtn, &QPushButton::pressed, this, [this]() { d->showTrustChainDialog(); }); connect(d->ui.moreDetailsBtn, &QPushButton::pressed, this, [this]() { d->showMoreDetails(); }); connect(d->ui.publishing, &QPushButton::pressed, this, [this]() { d->publishCertificate(); }); connect(d->ui.certifyBtn, &QPushButton::clicked, this, [this]() { d->certifyClicked(); }); connect(d->ui.webOfTrustBtn, &QPushButton::clicked, this, [this]() { d->webOfTrustClicked(); }); connect(d->ui.exportBtn, &QPushButton::clicked, this, [this]() { d->exportClicked(); }); connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, this, [this]() { d->keysMayHaveChanged(); }); } CertificateDetailsWidget::~CertificateDetailsWidget() { } -void CertificateDetailsWidget::setKey(const GpgME::Key &key) +void CertificateDetailsWidget::Private::setUpdatedKey(const GpgME::Key &k) { - d->key = key; - d->key.update(); // Fetch TOFU info (TODO: could be blocking, use async?) + key = k; - d->setupCommonProperties(); + setupCommonProperties(); if (key.protocol() == GpgME::OpenPGP) { - d->setupPGPProperties(); + setupPGPProperties(); } else { - d->setupSMIMEProperties(); + 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); + + connect(job, &QGpgME::KeyListJob::result, job, [this, job](GpgME::KeyListResult, std::vector keys, QString, GpgME::Error) { + d->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); + }); + + job->start(QStringList() << key.primaryFingerprint(), key.hasSecret()); +} + GpgME::Key CertificateDetailsWidget::key() const { return d->key; } CertificateDetailsDialog::CertificateDetailsDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18n("Certificate Details")); auto l = new QVBoxLayout(this); l->addWidget(new CertificateDetailsWidget(this)); auto bbox = new QDialogButtonBox(this); auto btn = bbox->addButton(QDialogButtonBox::Close); connect(btn, &QPushButton::pressed, this, &QDialog::accept); l->addWidget(bbox); readConfig(); } CertificateDetailsDialog::~CertificateDetailsDialog() { writeConfig(); } void CertificateDetailsDialog::readConfig() { KConfigGroup dialog(KSharedConfig::openConfig(), "CertificateDetailsDialog"); const QSize size = dialog.readEntry("Size", QSize(730, 280)); if (size.isValid()) { resize(size); } } void CertificateDetailsDialog::writeConfig() { KConfigGroup dialog(KSharedConfig::openConfig(), "CertificateDetailsDialog"); dialog.writeEntry("Size", size()); dialog.sync(); } void CertificateDetailsDialog::setKey(const GpgME::Key &key) { auto w = findChild(); Q_ASSERT(w); w->setKey(key); } GpgME::Key CertificateDetailsDialog::key() const { auto w = findChild(); Q_ASSERT(w); return w->key(); }