diff --git a/src/kleo/expirychecker.cpp b/src/kleo/expirychecker.cpp index fb08e2a9..4047a19f 100644 --- a/src/kleo/expirychecker.cpp +++ b/src/kleo/expirychecker.cpp @@ -1,545 +1,545 @@ /* This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 Sandro Knauß <sknauss@kde.org> SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> Based on kpgp.h Copyright (C) 2001,2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: LGPL-2.0-or-later */ #include "expirychecker.h" #include "debug.h" -#include "dn.h" #include "expirycheckersettings.h" #include <libkleo/algorithm.h> +#include <libkleo/formatting.h> #include <libkleo/keycache.h> #include <libkleo_debug.h> #include <KLocalizedString> #include <QGpgME/KeyListJob> #include <QGpgME/Protocol> #include <QTimeZone> #include <gpgme++/keylistresult.h> #include <set> #include <cmath> #include <ctime> using namespace Kleo; class Kleo::ExpiryCheckerPrivate { Kleo::ExpiryChecker *q; public: ExpiryCheckerPrivate(ExpiryChecker *qq, const ExpiryCheckerSettings &settings_) : q{qq} , settings{settings_} { } ExpiryChecker::Expiration calculateExpiration(const GpgME::Subkey &subkey) const; ExpiryChecker::Expiration checkForExpiration(const GpgME::Key &key, Kleo::chrono::days threshold, ExpiryChecker::CheckFlags flags) const; ExpiryChecker::Result checkKeyNearExpiry(const GpgME::Key &key, ExpiryChecker::CheckFlags flags); ExpiryCheckerSettings settings; std::set<QByteArray> alreadyWarnedFingerprints; std::shared_ptr<TimeProvider> timeProvider; }; ExpiryChecker::ExpiryChecker(const ExpiryCheckerSettings &settings, QObject *parent) : QObject{parent} , d{new ExpiryCheckerPrivate{this, settings}} { } ExpiryChecker::~ExpiryChecker() = default; ExpiryCheckerSettings ExpiryChecker::settings() const { return d->settings; } QString formatOpenPGPMessage(ExpiryChecker::Expiration expiration, ExpiryChecker::CheckFlags flags) { const GpgME::Key key = expiration.certificate; const bool isOwnKey = flags & ExpiryChecker::OwnKey; const bool isSigningKey = flags & ExpiryChecker::SigningKey; const auto keyInfo = ki18nc("<b>User ID of key</b> (Key ID key ID of key in hex notation)", "<b>%1</b> (Key ID 0x%2)") .subs(QString::fromUtf8(key.userID(0).id())) .subs(QString::fromLatin1(key.keyID())); if (expiration.status == ExpiryChecker::Expired) { qCDebug(LIBKLEO_LOG) << "Key" << key << "expired" << expiration.duration.count() << "days ago"; if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("<p>Your OpenPGP signing key</p><p align=center>%1</p><p>expired less than a day ago.</p>"); } else if (isOwnKey) { msg = ki18n("<p>Your OpenPGP encryption key</p><p align=center>%1</p><p>expired less than a day ago.</p>"); } else { msg = ki18n("<p>The OpenPGP key for</p><p align=center>%1</p><p>expired less than a day ago.</p>"); } return msg.subs(keyInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np("<p>Your OpenPGP signing key</p><p align=center>%2</p><p>expired yesterday.</p>", "<p>Your OpenPGP signing key</p><p align=center>%2</p><p>expired %1 days ago.</p>"); } else if (isOwnKey) { msg = ki18np("<p>Your OpenPGP encryption key</p><p align=center>%2</p><p>expired yesterday.</p>", "<p>Your OpenPGP encryption key</p><p align=center>%2</p><p>expired %1 days ago.</p>"); } else { msg = ki18np("<p>The OpenPGP key for</p><p align=center>%2</p><p>expired yesterday.</p>", "<p>The OpenPGP key for</p><p align=center>%2</p><p>expired %1 days ago.</p>"); } return msg.subs(expiration.duration.count()).subs(keyInfo).toString(); } qCDebug(LIBKLEO_LOG) << "Key" << key << "expires in" << expiration.duration.count() << "days"; if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("<p>Your OpenPGP signing key</p><p align=center>%1</p><p>expires today.</p>"); } else if (isOwnKey) { msg = ki18n("<p>Your OpenPGP encryption key</p><p align=center>%1</p><p>expires today.</p>"); } else { msg = ki18n("<p>The OpenPGP key for</p><p align=center>%1</p><p>expires today.</p>"); } return msg.subs(keyInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np("<p>Your OpenPGP signing key</p><p align=center>%2</p><p>expires tomorrow.</p>", "<p>Your OpenPGP signing key</p><p align=center>%2</p><p>expires in %1 days.</p>"); } else if (isOwnKey) { msg = ki18np("<p>Your OpenPGP encryption key</p><p align=center>%2</p><p>expires tomorrow.</p>", "<p>Your OpenPGP encryption key</p><p align=center>%2</p><p>expires in %1 days.</p>"); } else { msg = ki18np("<p>The OpenPGP key for</p><p align=center>%2</p><p>expires tomorrow.</p>", "<p>The OpenPGP key for</p><p align=center>%2</p><p>expires in %1 days.</p>"); } return msg.subs(expiration.duration.count()).subs(keyInfo).toString(); } QString formatSMIMEMessage(const GpgME::Key &orig_key, ExpiryChecker::Expiration expiration, ExpiryChecker::CheckFlags flags, bool ca) { const GpgME::Key key = expiration.certificate; const bool isOwnKey = flags & ExpiryChecker::OwnKey; const bool isSigningKey = flags & ExpiryChecker::SigningKey; const auto userCert = orig_key.isNull() ? key : orig_key; const auto userCertInfo = ki18nc("<b>User ID of certificate</b> (serial number serial no. of certificate)", "<b>%1</b> (serial number %2)") - .subs(Kleo::DN(userCert.userID(0).id()).prettyDN()) + .subs(Formatting::prettyDN(userCert.userID(0).id())) .subs(QString::fromLatin1(userCert.issuerSerial())); if (expiration.status == ExpiryChecker::Expired) { qCDebug(LIBKLEO_LOG) << "Certificate" << key << "expired" << expiration.duration.count() << "days ago"; if (ca) { if (key.isRoot()) { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "<p>The root certificate</p><p align=center><b>%2</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%1</p>" "<p>expired less than a day ago.</p>"); } else if (isOwnKey) { msg = ki18n( "<p>The root certificate</p><p align=center><b>%2</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%1</p>" "<p>expired less than a day ago.</p>"); } else { msg = ki18n( "<p>The root certificate</p><p align=center><b>%2</b></p>" "<p>for S/MIME certificate</p><p align=center>%1</p>" "<p>expired less than a day ago.</p>"); } - return msg.subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); + return msg.subs(userCertInfo).subs(Formatting::prettyDN(key.userID(0).id())).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expired yesterday.</p>", "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expired %1 days ago.</p>"); } else if (isOwnKey) { msg = ki18np( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expired yesterday.</p>", "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expired %1 days ago.</p>"); } else { msg = ki18np( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expired yesterday.</p>", "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expired %1 days ago.</p>"); } - return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); + return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Formatting::prettyDN(key.userID(0).id())).toString(); } else { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "<p>The intermediate CA certificate</p><p align=center><b>%2</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%1</p>" "<p>expired less than a day ago.</p>"); } else if (isOwnKey) { msg = ki18n( "<p>The intermediate CA certificate</p><p align=center><b>%2</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%1</p>" "<p>expired less than a day ago.</p>"); } else { msg = ki18n( "<p>The intermediate CA certificate</p><p align=center><b>%2</b></p>" "<p>for S/MIME certificate</p><p align=center>%1</p>" "<p>expired less than a day ago.</p>"); } - return msg.subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); + return msg.subs(userCertInfo).subs(Formatting::prettyDN(key.userID(0).id())).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expired yesterday.</p>", "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expired %1 days ago.</p>"); } else if (isOwnKey) { msg = ki18np( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expired yesterday.</p>", "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expired %1 days ago.</p>"); } else { msg = ki18np( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expired yesterday.</p>", "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expired %1 days ago.</p>"); } - return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); + return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Formatting::prettyDN(key.userID(0).id())).toString(); } } else { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("<p>Your S/MIME signing certificate</p><p align=center>%1</p><p>expired less than a day ago.</p>"); } else if (isOwnKey) { msg = ki18n("<p>Your S/MIME encryption certificate</p><p align=center>%1</p><p>expired less than a day ago.</p>"); } else { msg = ki18n("<p>The S/MIME certificate for</p><p align=center>%1</p><p>expired less than a day ago.</p>"); } return msg.subs(userCertInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np("<p>Your S/MIME signing certificate</p><p align=center>%2</p><p>expired yesterday.</p>", "<p>Your S/MIME signing certificate</p><p align=center>%2</p><p>expired %1 days ago.</p>"); } else if (isOwnKey) { msg = ki18np("<p>Your S/MIME encryption certificate</p><p align=center>%2</p><p>expired yesterday.</p>", "<p>Your S/MIME encryption certificate</p><p align=center>%2</p><p>expired %1 days ago.</p>"); } else { msg = ki18np("<p>The S/MIME certificate for</p><p align=center>%2</p><p>expired yesterday.</p>", "<p>The S/MIME certificate for</p><p align=center>%2</p><p>expired %1 days ago.</p>"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).toString(); } } qCDebug(LIBKLEO_LOG) << "Certificate" << key << "expires in" << expiration.duration.count() << "days"; if (ca) { if (key.isRoot()) { if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires today.</p>"); } else if (isOwnKey) { msg = ki18n( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires today.</p>"); } else { msg = ki18n( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expires today.</p>"); } - return msg.subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); + return msg.subs(userCertInfo).subs(Formatting::prettyDN(key.userID(0).id())).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } else if (isOwnKey) { msg = ki18np( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } else { msg = ki18np( "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>The root certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } - return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); + return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Formatting::prettyDN(key.userID(0).id())).toString(); } if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires today.</p>"); } else if (isOwnKey) { msg = ki18n( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires today.</p>"); } else { msg = ki18n( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expires today.</p>"); } } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } else if (isOwnKey) { msg = ki18np( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } else { msg = ki18np( "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>The intermediate CA certificate</p><p align=center><b>%3</b></p>" "<p>for S/MIME certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } - return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Kleo::DN(key.userID(0).id()).prettyDN()).toString(); + return msg.subs(expiration.duration.count()).subs(userCertInfo).subs(Formatting::prettyDN(key.userID(0).id())).toString(); } if (expiration.duration.count() == 0) { KLocalizedString msg; if (isSigningKey) { msg = ki18n("<p>Your S/MIME signing certificate</p><p align=center>%2</p><p>expires today.</p>"); } else if (isOwnKey) { msg = ki18n("<p>Your S/MIME encryption certificate</p><p align=center>%2</p><p>expires today.</p>"); } else { msg = ki18n("<p>The S/MIME certificate for</p><p align=center>%2</p><p>expires today.</p>"); } return msg.subs(userCertInfo).toString(); } KLocalizedString msg; if (isSigningKey) { msg = ki18np( "<p>Your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>Your S/MIME signing certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } else if (isOwnKey) { msg = ki18np( "<p>Your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>Your S/MIME encryption certificate</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } else { msg = ki18np( "<p>The S/MIME certificate for</p><p align=center>%2</p>" "<p>expires tomorrow.</p>", "<p>The S/MIME certificate for</p><p align=center>%2</p>" "<p>expires in %1 days.</p>"); } return msg.subs(expiration.duration.count()).subs(userCertInfo).toString(); } static GpgME::Subkey findBestSubkey(const GpgME::Key &key, ExpiryChecker::CheckFlags usageFlags) { // find the subkey with the latest expiration date for the given usage flags if (!(usageFlags & ExpiryChecker::UsageMask)) { // return primary key if no specific usage is specified (as for chain certificates) return key.subkey(0); } GpgME::Subkey result; for (unsigned int i = 0; i < key.numSubkeys(); ++i) { const auto subkey = key.subkey(i); if (subkey.isRevoked() || subkey.isInvalid() || subkey.isDisabled()) { // unusable subkey continue; } if (((usageFlags & ExpiryChecker::EncryptionKey) && !subkey.canEncrypt()) // || ((usageFlags & ExpiryChecker::SigningKey) && !subkey.canSign()) // || ((usageFlags & ExpiryChecker::CertificationKey) && !subkey.canCertify())) { // unsuitable subkey for requested usage continue; } if (subkey.neverExpires()) { // stop looking for the best subkey if we found a suitable subkey that doesn't expire; // return the primary key because a non-expiring subkey inherits the primary key's expiration return key.subkey(0); } if (quint32(subkey.expirationTime()) > quint32(result.expirationTime())) { result = subkey; } } return result; } ExpiryChecker::Expiration ExpiryCheckerPrivate::calculateExpiration(const GpgME::Subkey &subkey) const { if (subkey.neverExpires()) { return {subkey.parent(), ExpiryChecker::NotNearExpiry, Kleo::chrono::days::zero()}; } const qint64 currentTime = timeProvider ? timeProvider->currentTime() : QDateTime::currentSecsSinceEpoch(); const auto currentDate = timeProvider ? timeProvider->currentDate() : QDate::currentDate(); const auto timeZone = timeProvider ? timeProvider->timeZone() : QTimeZone{QTimeZone::LocalTime}; // interpret the expiration time as unsigned 32-bit value if it's negative; gpg also uses uint32 internally const qint64 expirationTime = qint64(subkey.expirationTime() < 0 ? quint32(subkey.expirationTime()) : subkey.expirationTime()); const auto expirationDate = QDateTime::fromSecsSinceEpoch(expirationTime, timeZone).date(); if (expirationTime <= currentTime) { return {subkey.parent(), ExpiryChecker::Expired, Kleo::chrono::days{expirationDate.daysTo(currentDate)}}; } else { return {subkey.parent(), ExpiryChecker::ExpiresSoon, Kleo::chrono::days{currentDate.daysTo(expirationDate)}}; } } ExpiryChecker::Expiration ExpiryCheckerPrivate::checkForExpiration(const GpgME::Key &key, // Kleo::chrono::days threshold, ExpiryChecker::CheckFlags usageFlags) const { const auto subkey = findBestSubkey(key, usageFlags); if (subkey.isNull()) { return {key, ExpiryChecker::NoSuitableSubkey, {}}; } ExpiryChecker::Expiration expiration = calculateExpiration(subkey); if ((expiration.status == ExpiryChecker::ExpiresSoon) && (expiration.duration > threshold)) { // key expires, but not too soon expiration.status = ExpiryChecker::NotNearExpiry; } return expiration; } ExpiryChecker::Result ExpiryCheckerPrivate::checkKeyNearExpiry(const GpgME::Key &orig_key, ExpiryChecker::CheckFlags flags) { static const int maximumCertificateChainLength = 100; const bool isOwnKey = flags & ExpiryChecker::OwnKey; ExpiryChecker::Result result; result.checkFlags = flags; result.expiration.certificate = orig_key; // use vector instead of set because certificate chains are usually very short std::vector<std::string> checkedCertificates; auto key = orig_key; for (int chainCount = 0; chainCount < maximumCertificateChainLength; ++chainCount) { checkedCertificates.push_back(key.primaryFingerprint()); const GpgME::Subkey subkey = key.subkey(0); const bool newMessage = !alreadyWarnedFingerprints.count(subkey.fingerprint()); const auto threshold = chainCount > 0 // ? (key.isRoot() ? settings.rootCertThreshold() : settings.chainCertThreshold()) // : (isOwnKey ? settings.ownKeyThreshold() : settings.otherKeyThreshold()); const auto usageFlags = (chainCount == 0) ? (flags & ExpiryChecker::UsageMask) : ExpiryChecker::CheckFlags{}; const auto expiration = checkForExpiration(key, threshold, usageFlags); if (chainCount == 0) { result.expiration = expiration; } else if (expiration.status != ExpiryChecker::NotNearExpiry) { result.chainExpiration.push_back(expiration); } if (expiration.status == ExpiryChecker::Expired) { const QString msg = key.protocol() == GpgME::OpenPGP // ? formatOpenPGPMessage(expiration, flags) : formatSMIMEMessage(orig_key, expiration, flags, chainCount > 0); alreadyWarnedFingerprints.insert(subkey.fingerprint()); Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyExpired : ExpiryChecker::OtherKeyExpired, newMessage); } else if (expiration.status == ExpiryChecker::ExpiresSoon) { const QString msg = key.protocol() == GpgME::OpenPGP // ? formatOpenPGPMessage(expiration, flags) : formatSMIMEMessage(orig_key, expiration, flags, chainCount > 0); alreadyWarnedFingerprints.insert(subkey.fingerprint()); Q_EMIT q->expiryMessage(key, msg, isOwnKey ? ExpiryChecker::OwnKeyNearExpiry : ExpiryChecker::OtherKeyNearExpiry, newMessage); } else if (expiration.status == ExpiryChecker::NoSuitableSubkey) { break; } if (!(flags & ExpiryChecker::CheckChain) || key.isRoot() || (key.protocol() != GpgME::CMS)) { break; } const auto keys = KeyCache::instance()->findIssuers(key, KeyCache::NoOption); if (keys.empty()) { break; } key = keys.front(); if (Kleo::contains(checkedCertificates, key.primaryFingerprint())) { break; // this certificate was already checked (looks like a circle in the chain) } } return result; } ExpiryChecker::Result ExpiryChecker::checkKey(const GpgME::Key &key, CheckFlags flags) const { if (key.isNull()) { qWarning(LIBKLEO_LOG) << __func__ << "called with null key"; return {flags, {key, InvalidKey, {}}, {}}; } if (!(flags & UsageMask)) { qWarning(LIBKLEO_LOG) << __func__ << "called with invalid flags:" << flags; return {flags, {key, InvalidCheckFlags, {}}, {}}; } return d->checkKeyNearExpiry(key, flags); } void ExpiryChecker::setTimeProviderForTest(const std::shared_ptr<TimeProvider> &timeProvider) { d->timeProvider = timeProvider; } #include "moc_expirychecker.cpp" diff --git a/src/ui/keyrequester.cpp b/src/ui/keyrequester.cpp index b7fa5e91..164fac67 100644 --- a/src/ui/keyrequester.cpp +++ b/src/ui/keyrequester.cpp @@ -1,516 +1,515 @@ /* -*- c++ -*- keyrequester.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB Based on kpgpui.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details This file is part of KPGP, the KDE PGP/GnuPG support library. SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-libkleo.h> #include "keyrequester.h" #include "keyselectiondialog.h" #include <libkleo/algorithm.h> #include <libkleo/compliance.h> -#include <libkleo/dn.h> #include <libkleo/formatting.h> #include <libkleo/keyhelpers.h> #include <KLocalizedString> #include <KMessageBox> #include <QGpgME/KeyListJob> #include <QApplication> #include <QDialog> #include <QHBoxLayout> #include <QPushButton> #include <QString> #include <gpgme++/key.h> #include <gpgme++/keylistresult.h> using namespace QGpgME; using namespace Kleo; Kleo::KeyRequester::KeyRequester(unsigned int allowedKeys, bool multipleKeys, QWidget *parent) : QWidget(parent) , mOpenPGPBackend(nullptr) , mSMIMEBackend(nullptr) , mMulti(multipleKeys) , mKeyUsage(allowedKeys) , mJobs(0) , d(nullptr) { init(); } Kleo::KeyRequester::KeyRequester(QWidget *parent) : QWidget(parent) , mOpenPGPBackend(nullptr) , mSMIMEBackend(nullptr) , mMulti(false) , mKeyUsage(0) , mJobs(0) , d(nullptr) { init(); } void Kleo::KeyRequester::init() { auto hlay = new QHBoxLayout(this); hlay->setContentsMargins(0, 0, 0, 0); if (DeVSCompliance::isCompliant()) { mComplianceIcon = new QLabel{this}; mComplianceIcon->setPixmap(Formatting::questionIcon().pixmap(22)); } // the label where the key id is to be displayed: mLabel = new QLabel(this); mLabel->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); // the button to unset any key: mEraseButton = new QPushButton(this); mEraseButton->setAutoDefault(false); mEraseButton->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); mEraseButton->setIcon( QIcon::fromTheme(QApplication::isRightToLeft() ? QStringLiteral("edit-clear-locationbar-ltr") : QStringLiteral("edit-clear-locationbar-rtl"))); mEraseButton->setToolTip(i18nc("@info:tooltip", "Clear")); // the button to call the KeySelectionDialog: mDialogButton = new QPushButton(i18nc("@action:button", "Change..."), this); mDialogButton->setAutoDefault(false); if (mComplianceIcon) { hlay->addWidget(mComplianceIcon); } hlay->addWidget(mLabel, 1); hlay->addWidget(mEraseButton); hlay->addWidget(mDialogButton); connect(mEraseButton, &QPushButton::clicked, this, &SigningKeyRequester::slotEraseButtonClicked); connect(mDialogButton, &QPushButton::clicked, this, &SigningKeyRequester::slotDialogButtonClicked); setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed)); setAllowedKeys(mKeyUsage); } Kleo::KeyRequester::~KeyRequester() { } const std::vector<GpgME::Key> &Kleo::KeyRequester::keys() const { return mKeys; } const GpgME::Key &Kleo::KeyRequester::key() const { static const GpgME::Key null = GpgME::Key::null; if (mKeys.empty()) { return null; } else { return mKeys.front(); } } void Kleo::KeyRequester::setKeys(const std::vector<GpgME::Key> &keys) { mKeys.clear(); for (auto it = keys.begin(); it != keys.end(); ++it) { if (!it->isNull()) { mKeys.push_back(*it); } } updateKeys(); } void Kleo::KeyRequester::setKey(const GpgME::Key &key) { mKeys.clear(); if (!key.isNull()) { mKeys.push_back(key); } updateKeys(); } QString Kleo::KeyRequester::fingerprint() const { if (mKeys.empty()) { return QString(); } else { return QLatin1StringView(mKeys.front().primaryFingerprint()); } } QStringList Kleo::KeyRequester::fingerprints() const { QStringList result; for (auto it = mKeys.begin(); it != mKeys.end(); ++it) { if (!it->isNull()) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1StringView(fpr)); } } } return result; } void Kleo::KeyRequester::setFingerprint(const QString &fingerprint) { startKeyListJob(QStringList(fingerprint)); } void Kleo::KeyRequester::setFingerprints(const QStringList &fingerprints) { startKeyListJob(fingerprints); } void Kleo::KeyRequester::updateKeys() { if (mKeys.empty()) { if (mComplianceIcon) { mComplianceIcon->setPixmap(Formatting::unavailableIcon().pixmap(22)); mComplianceIcon->setToolTip(QString{}); } mLabel->clear(); return; } if (mKeys.size() > 1) { setMultipleKeysEnabled(true); } QStringList labelTexts; QString toolTipText; for (std::vector<GpgME::Key>::const_iterator it = mKeys.begin(); it != mKeys.end(); ++it) { if (it->isNull()) { continue; } const QString fpr = QLatin1StringView(it->primaryFingerprint()); const QString keyID = QString::fromLatin1(it->keyID()); labelTexts.push_back(keyID); toolTipText += keyID + QLatin1StringView(": "); if (const char *uid = it->userID(0).id()) { if (it->protocol() == GpgME::OpenPGP) { toolTipText += QString::fromUtf8(uid); } else { - toolTipText += Kleo::DN(uid).prettyDN(); + toolTipText += Formatting::prettyDN(uid); } } else { toolTipText += xi18n("<placeholder>unknown</placeholder>"); } toolTipText += QLatin1Char('\n'); } if (mComplianceIcon) { if (Kleo::all_of(mKeys, &Kleo::DeVSCompliance::keyIsCompliant)) { mComplianceIcon->setPixmap(Formatting::successIcon().pixmap(22)); mComplianceIcon->setToolTip(DeVSCompliance::name(true)); } else { mComplianceIcon->setPixmap(Formatting::warningIcon().pixmap(22)); mComplianceIcon->setToolTip(DeVSCompliance::name(false)); } } mLabel->setText(labelTexts.join(QLatin1StringView(", "))); mLabel->setToolTip(toolTipText); } #ifndef __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ #define __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ static void showKeyListError(QWidget *parent, const GpgME::Error &err) { Q_ASSERT(err); const QString msg = i18n( "<qt><p>An error occurred while fetching " "the keys from the backend:</p>" "<p><b>%1</b></p></qt>", Formatting::errorAsString(err)); KMessageBox::error(parent, msg, i18nc("@title:window", "Key Listing Failed")); } #endif // __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ void Kleo::KeyRequester::startKeyListJob(const QStringList &fingerprints) { if (!mSMIMEBackend && !mOpenPGPBackend) { return; } mTmpKeys.clear(); mJobs = 0; unsigned int count = 0; for (QStringList::const_iterator it = fingerprints.begin(); it != fingerprints.end(); ++it) { if (!(*it).trimmed().isEmpty()) { ++count; } } if (!count) { // don't fall into the trap that an empty pattern means // "return all keys" :) setKey(GpgME::Key::null); return; } if (mOpenPGPBackend) { KeyListJob *job = mOpenPGPBackend->keyListJob(false); // local, no sigs if (!job) { KMessageBox::error(this, i18n("The OpenPGP backend does not support listing keys. " "Check your installation."), i18nc("@title:window", "Key Listing Failed")); } else { connect(job, &KeyListJob::result, this, &SigningKeyRequester::slotKeyListResult); connect(job, &KeyListJob::nextKey, this, &SigningKeyRequester::slotNextKey); const GpgME::Error err = job->start(fingerprints, mKeyUsage & Kleo::KeySelectionDialog::SecretKeys && !(mKeyUsage & Kleo::KeySelectionDialog::PublicKeys)); if (err) { showKeyListError(this, err); } else { ++mJobs; } } } if (mSMIMEBackend) { KeyListJob *job = mSMIMEBackend->keyListJob(false); // local, no sigs if (!job) { KMessageBox::error(this, i18n("The S/MIME backend does not support listing keys. " "Check your installation."), i18nc("@title:window", "Key Listing Failed")); } else { connect(job, &KeyListJob::result, this, &SigningKeyRequester::slotKeyListResult); connect(job, &KeyListJob::nextKey, this, &SigningKeyRequester::slotNextKey); const GpgME::Error err = job->start(fingerprints, mKeyUsage & Kleo::KeySelectionDialog::SecretKeys && !(mKeyUsage & Kleo::KeySelectionDialog::PublicKeys)); if (err) { showKeyListError(this, err); } else { ++mJobs; } } } if (mJobs > 0) { mEraseButton->setEnabled(false); mDialogButton->setEnabled(false); } } void Kleo::KeyRequester::slotNextKey(const GpgME::Key &key) { if (!key.isNull()) { mTmpKeys.push_back(key); } } void Kleo::KeyRequester::slotKeyListResult(const GpgME::KeyListResult &res) { if (res.error()) { showKeyListError(this, res.error()); } if (--mJobs <= 0) { mEraseButton->setEnabled(true); mDialogButton->setEnabled(true); setKeys(mTmpKeys); mTmpKeys.clear(); } } void Kleo::KeyRequester::slotDialogButtonClicked() { KeySelectionDialog *dlg = mKeys.empty() ? new KeySelectionDialog(mDialogCaption, mDialogMessage, mInitialQuery, mKeyUsage, mMulti, false, this) : new KeySelectionDialog(mDialogCaption, mDialogCaption, mKeys, mKeyUsage, mMulti, false, this); if (dlg->exec() == QDialog::Accepted) { if (mMulti) { setKeys(dlg->selectedKeys()); } else { setKey(dlg->selectedKey()); } Q_EMIT changed(); } delete dlg; } void Kleo::KeyRequester::slotEraseButtonClicked() { if (!mKeys.empty()) { Q_EMIT changed(); } mKeys.clear(); updateKeys(); } void Kleo::KeyRequester::setDialogCaption(const QString &caption) { mDialogCaption = caption; } void Kleo::KeyRequester::setDialogMessage(const QString &msg) { mDialogMessage = msg; } bool Kleo::KeyRequester::isMultipleKeysEnabled() const { return mMulti; } void Kleo::KeyRequester::setMultipleKeysEnabled(bool multi) { if (multi == mMulti) { return; } if (!multi && !mKeys.empty()) { mKeys.erase(mKeys.begin() + 1, mKeys.end()); } mMulti = multi; updateKeys(); } unsigned int Kleo::KeyRequester::allowedKeys() const { return mKeyUsage; } void Kleo::KeyRequester::setAllowedKeys(unsigned int keyUsage) { mKeyUsage = keyUsage; mOpenPGPBackend = nullptr; mSMIMEBackend = nullptr; if (mKeyUsage & KeySelectionDialog::OpenPGPKeys) { mOpenPGPBackend = openpgp(); } if (mKeyUsage & KeySelectionDialog::SMIMEKeys) { mSMIMEBackend = smime(); } if (mOpenPGPBackend && !mSMIMEBackend) { mDialogCaption = i18n("OpenPGP Key Selection"); mDialogMessage = i18n("Please select an OpenPGP key to use."); } else if (!mOpenPGPBackend && mSMIMEBackend) { mDialogCaption = i18n("S/MIME Key Selection"); mDialogMessage = i18n("Please select an S/MIME key to use."); } else { mDialogCaption = i18n("Key Selection"); mDialogMessage = i18n("Please select an (OpenPGP or S/MIME) key to use."); } } QPushButton *Kleo::KeyRequester::dialogButton() { return mDialogButton; } QPushButton *Kleo::KeyRequester::eraseButton() { return mEraseButton; } static inline unsigned int foo(bool openpgp, bool smime, bool trusted, bool valid) { unsigned int result = 0; if (openpgp) { result |= Kleo::KeySelectionDialog::OpenPGPKeys; } if (smime) { result |= Kleo::KeySelectionDialog::SMIMEKeys; } if (trusted) { result |= Kleo::KeySelectionDialog::TrustedKeys; } if (valid) { result |= Kleo::KeySelectionDialog::ValidKeys; } return result; } static inline unsigned int encryptionKeyUsage(bool openpgp, bool smime, bool trusted, bool valid) { return foo(openpgp, smime, trusted, valid) | Kleo::KeySelectionDialog::EncryptionKeys | Kleo::KeySelectionDialog::PublicKeys; } static inline unsigned int signingKeyUsage(bool openpgp, bool smime, bool trusted, bool valid) { return foo(openpgp, smime, trusted, valid) | Kleo::KeySelectionDialog::SigningKeys | Kleo::KeySelectionDialog::SecretKeys; } Kleo::EncryptionKeyRequester::EncryptionKeyRequester(bool multi, unsigned int proto, QWidget *parent, bool onlyTrusted, bool onlyValid) : KeyRequester(encryptionKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid), multi, parent) , d(nullptr) { } Kleo::EncryptionKeyRequester::EncryptionKeyRequester(QWidget *parent) : KeyRequester(0, false, parent) , d(nullptr) { } Kleo::EncryptionKeyRequester::~EncryptionKeyRequester() { } void Kleo::EncryptionKeyRequester::setAllowedKeys(unsigned int proto, bool onlyTrusted, bool onlyValid) { KeyRequester::setAllowedKeys(encryptionKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid)); } Kleo::SigningKeyRequester::SigningKeyRequester(bool multi, unsigned int proto, QWidget *parent, bool onlyTrusted, bool onlyValid) : KeyRequester(signingKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid), multi, parent) , d(nullptr) { } Kleo::SigningKeyRequester::SigningKeyRequester(QWidget *parent) : KeyRequester(0, false, parent) , d(nullptr) { } Kleo::SigningKeyRequester::~SigningKeyRequester() { } void Kleo::SigningKeyRequester::setAllowedKeys(unsigned int proto, bool onlyTrusted, bool onlyValid) { KeyRequester::setAllowedKeys(signingKeyUsage(proto & OpenPGP, proto & SMIME, onlyTrusted, onlyValid)); } void Kleo::KeyRequester::virtual_hook(int, void *) { } void Kleo::EncryptionKeyRequester::virtual_hook(int id, void *data) { KeyRequester::virtual_hook(id, data); } void Kleo::SigningKeyRequester::virtual_hook(int id, void *data) { KeyRequester::virtual_hook(id, data); } #include "moc_keyrequester.cpp" diff --git a/src/ui/keyselectiondialog.cpp b/src/ui/keyselectiondialog.cpp index ee257409..fbed3e67 100644 --- a/src/ui/keyselectiondialog.cpp +++ b/src/ui/keyselectiondialog.cpp @@ -1,1015 +1,1014 @@ /* -*- c++ -*- keyselectiondialog.cpp This file is part of libkleopatra, the KDE keymanagement library SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB Based on kpgpui.cpp SPDX-FileCopyrightText: 2001, 2002 the KPGP authors See file libkdenetwork/AUTHORS.kpgp for details SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-libkleo.h> #include "keyselectiondialog.h" #include "keylistview.h" #include "progressdialog.h" #include <libkleo/compat.h> #include <libkleo/compliance.h> -#include <libkleo/dn.h> #include <libkleo/formatting.h> #include <kleo_ui_debug.h> #include <KConfig> #include <KConfigGroup> #include <KLocalizedString> #include <KMessageBox> #include <KSharedConfig> #include <QGpgME/KeyListJob> #include <QApplication> #include <QCheckBox> #include <QDialogButtonBox> #include <QFrame> #include <QHBoxLayout> #include <QLabel> #include <QLineEdit> #include <QMenu> #include <QProcess> #include <QPushButton> #include <QRegularExpression> #include <QScrollBar> #include <QTimer> #include <QVBoxLayout> #include <gpgme++/key.h> #include <gpgme++/keylistresult.h> #include <algorithm> #include <iterator> #include <string.h> using namespace Kleo; static bool checkKeyUsage(const GpgME::Key &key, unsigned int keyUsage, QString *statusString = nullptr) { auto setStatusString = [statusString](const QString &status) { if (statusString) { *statusString = status; } }; if (keyUsage & KeySelectionDialog::ValidKeys) { if (key.isInvalid()) { if (key.keyListMode() & GpgME::Validate) { qCDebug(KLEO_UI_LOG) << "key is invalid"; setStatusString(i18n("The key is not valid.")); return false; } else { qCDebug(KLEO_UI_LOG) << "key is invalid - ignoring"; } } if (key.isExpired()) { qCDebug(KLEO_UI_LOG) << "key is expired"; setStatusString(i18n("The key is expired.")); return false; } else if (key.isRevoked()) { qCDebug(KLEO_UI_LOG) << "key is revoked"; setStatusString(i18n("The key is revoked.")); return false; } else if (key.isDisabled()) { qCDebug(KLEO_UI_LOG) << "key is disabled"; setStatusString(i18n("The key is disabled.")); return false; } } if (keyUsage & KeySelectionDialog::EncryptionKeys && !Kleo::keyHasEncrypt(key)) { qCDebug(KLEO_UI_LOG) << "key can't encrypt"; setStatusString(i18n("The key is not designated for encryption.")); return false; } if (keyUsage & KeySelectionDialog::SigningKeys && !Kleo::keyHasSign(key)) { qCDebug(KLEO_UI_LOG) << "key can't sign"; setStatusString(i18n("The key is not designated for signing.")); return false; } if (keyUsage & KeySelectionDialog::CertificationKeys && !Kleo::keyHasCertify(key)) { qCDebug(KLEO_UI_LOG) << "key can't certify"; setStatusString(i18n("The key is not designated for certifying.")); return false; } if (keyUsage & KeySelectionDialog::AuthenticationKeys && !Kleo::keyHasAuthenticate(key)) { qCDebug(KLEO_UI_LOG) << "key can't authenticate"; setStatusString(i18n("The key is not designated for authentication.")); return false; } if (keyUsage & KeySelectionDialog::SecretKeys && !(keyUsage & KeySelectionDialog::PublicKeys) && !key.hasSecret()) { qCDebug(KLEO_UI_LOG) << "key isn't secret"; setStatusString(i18n("The key is not secret.")); return false; } if (keyUsage & KeySelectionDialog::TrustedKeys && key.protocol() == GpgME::OpenPGP && // only check this for secret keys for now. // Seems validity isn't checked for secret keylistings... !key.hasSecret()) { std::vector<GpgME::UserID> uids = key.userIDs(); for (std::vector<GpgME::UserID>::const_iterator it = uids.begin(); it != uids.end(); ++it) { if (!it->isRevoked() && it->validity() >= GpgME::UserID::Marginal) { setStatusString(i18n("The key can be used.")); return true; } } qCDebug(KLEO_UI_LOG) << "key has no UIDs with validity >= Marginal"; setStatusString(i18n("The key is not trusted enough.")); return false; } // X.509 keys are always trusted, else they won't be the keybox. // PENDING(marc) check that this ^ is correct setStatusString(i18n("The key can be used.")); return true; } static bool checkKeyUsage(const std::vector<GpgME::Key> &keys, unsigned int keyUsage) { for (auto it = keys.begin(); it != keys.end(); ++it) { if (!checkKeyUsage(*it, keyUsage)) { return false; } } return true; } namespace { class ColumnStrategy : public KeyListView::ColumnStrategy { public: ColumnStrategy(unsigned int keyUsage); QString title(int col) const override; int width(int col, const QFontMetrics &fm) const override; QString text(const GpgME::Key &key, int col) const override; QString accessibleText(const GpgME::Key &key, int column) const override; QString toolTip(const GpgME::Key &key, int col) const override; QIcon icon(const GpgME::Key &key, int col) const override; private: const QIcon mKeyGoodPix, mKeyBadPix, mKeyUnknownPix, mKeyValidPix; const unsigned int mKeyUsage; }; ColumnStrategy::ColumnStrategy(unsigned int keyUsage) : KeyListView::ColumnStrategy() , mKeyGoodPix(QStringLiteral(":/libkleopatra/key_ok")) , mKeyBadPix(QStringLiteral(":/libkleopatra/key_bad")) , mKeyUnknownPix(QStringLiteral(":/libkleopatra/key_unknown")) , mKeyValidPix(QStringLiteral(":/libkleopatra/key")) , mKeyUsage(keyUsage) { if (keyUsage == 0) { qCWarning(KLEO_UI_LOG) << "KeySelectionDialog: keyUsage == 0. You want to use AllKeys instead."; } } QString ColumnStrategy::title(int col) const { switch (col) { case 0: return i18n("Key ID"); case 1: return i18n("User ID"); default: return QString(); } } int ColumnStrategy::width(int col, const QFontMetrics &fm) const { if (col == 0) { static const char hexchars[] = "0123456789ABCDEF"; int maxWidth = 0; for (unsigned int i = 0; i < 16; ++i) { maxWidth = qMax(fm.boundingRect(QLatin1Char(hexchars[i])).width(), maxWidth); } return 8 * maxWidth + 2 * 16 /* KIconLoader::SizeSmall */; } return KeyListView::ColumnStrategy::width(col, fm); } QString ColumnStrategy::text(const GpgME::Key &key, int col) const { switch (col) { case 0: { if (key.keyID()) { return Formatting::prettyID(key.keyID()); } else { return xi18n("<placeholder>unknown</placeholder>"); } } case 1: { const char *uid = key.userID(0).id(); if (key.protocol() == GpgME::OpenPGP) { return uid && *uid ? QString::fromUtf8(uid) : QString(); } else { // CMS - return DN(uid).prettyDN(); + return Formatting::prettyDN(uid); } } default: return QString(); } } QString ColumnStrategy::accessibleText(const GpgME::Key &key, int col) const { switch (col) { case 0: { if (key.keyID()) { return Formatting::accessibleHexID(key.keyID()); } [[fallthrough]]; } default: return {}; } } QString ColumnStrategy::toolTip(const GpgME::Key &key, int) const { const char *uid = key.userID(0).id(); const char *fpr = key.primaryFingerprint(); const char *issuer = key.issuerName(); const GpgME::Subkey subkey = key.subkey(0); const QString expiry = Formatting::expirationDateString(subkey); const QString creation = Formatting::creationDateString(subkey); QString keyStatusString; if (!checkKeyUsage(key, mKeyUsage, &keyStatusString)) { // Show the status in bold if there is a problem keyStatusString = QLatin1StringView("<b>") % keyStatusString % QLatin1StringView("</b>"); } QString html = QStringLiteral("<qt><p style=\"style='white-space:pre'\">"); if (key.protocol() == GpgME::OpenPGP) { html += i18n("OpenPGP key for <b>%1</b>", uid ? QString::fromUtf8(uid) : i18n("unknown")); } else { - html += i18n("S/MIME key for <b>%1</b>", uid ? DN(uid).prettyDN() : i18n("unknown")); + html += i18n("S/MIME key for <b>%1</b>", uid ? Formatting::prettyDN(uid) : i18n("unknown")); } html += QStringLiteral("</p><table>"); const auto addRow = [&html](const QString &name, const QString &value) { html += QStringLiteral("<tr><td align=\"right\"><b>%1: </b></td><td>%2</td></tr>").arg(name, value); }; addRow(i18n("Valid from"), creation); addRow(i18n("Valid until"), expiry); addRow(i18nc("Key fingerprint", "Fingerprint"), fpr ? QString::fromLatin1(fpr) : i18n("unknown")); if (key.protocol() != GpgME::OpenPGP) { - addRow(i18nc("Key issuer", "Issuer"), issuer ? DN(issuer).prettyDN() : i18n("unknown")); + addRow(i18nc("Key issuer", "Issuer"), issuer ? Formatting::prettyDN(issuer) : i18n("unknown")); } addRow(i18nc("Key status", "Status"), keyStatusString); if (DeVSCompliance::isActive()) { addRow(i18nc("Compliance of key", "Compliance"), DeVSCompliance::name(key.isDeVs())); } html += QStringLiteral("</table></qt>"); return html; } QIcon ColumnStrategy::icon(const GpgME::Key &key, int col) const { if (col != 0) { return QIcon(); } // this key did not undergo a validating keylisting yet: if (!(key.keyListMode() & GpgME::Validate)) { return mKeyUnknownPix; } if (!checkKeyUsage(key, mKeyUsage)) { return mKeyBadPix; } if (key.protocol() == GpgME::CMS) { return mKeyGoodPix; } switch (key.userID(0).validity()) { default: case GpgME::UserID::Unknown: case GpgME::UserID::Undefined: return mKeyUnknownPix; case GpgME::UserID::Never: return mKeyValidPix; case GpgME::UserID::Marginal: case GpgME::UserID::Full: case GpgME::UserID::Ultimate: { if (DeVSCompliance::isActive() && !key.isDeVs()) { return mKeyValidPix; } return mKeyGoodPix; } } } } static const int sCheckSelectionDelay = 250; KeySelectionDialog::KeySelectionDialog(QWidget *parent, Options options) : QDialog(parent) , mOpenPGPBackend(QGpgME::openpgp()) , mSMIMEBackend(QGpgME::smime()) , mKeyUsage(AllKeys) { qCDebug(KLEO_UI_LOG) << "mTruncated:" << mTruncated << "mSavedOffsetY:" << mSavedOffsetY; setUpUI(options, QString()); } KeySelectionDialog::KeySelectionDialog(const QString &title, const QString &text, const std::vector<GpgME::Key> &selectedKeys, unsigned int keyUsage, bool extendedSelection, bool rememberChoice, QWidget *parent, bool modal) : QDialog(parent) , mSelectedKeys(selectedKeys) , mKeyUsage(keyUsage) { setWindowTitle(title); setModal(modal); init(rememberChoice, extendedSelection, text, QString()); } KeySelectionDialog::KeySelectionDialog(const QString &title, const QString &text, const QString &initialQuery, const std::vector<GpgME::Key> &selectedKeys, unsigned int keyUsage, bool extendedSelection, bool rememberChoice, QWidget *parent, bool modal) : QDialog(parent) , mSelectedKeys(selectedKeys) , mKeyUsage(keyUsage) , mSearchText(initialQuery) , mInitialQuery(initialQuery) { setWindowTitle(title); setModal(modal); init(rememberChoice, extendedSelection, text, initialQuery); } KeySelectionDialog::KeySelectionDialog(const QString &title, const QString &text, const QString &initialQuery, unsigned int keyUsage, bool extendedSelection, bool rememberChoice, QWidget *parent, bool modal) : QDialog(parent) , mKeyUsage(keyUsage) , mSearchText(initialQuery) , mInitialQuery(initialQuery) { setWindowTitle(title); setModal(modal); init(rememberChoice, extendedSelection, text, initialQuery); } void KeySelectionDialog::setUpUI(Options options, const QString &initialQuery) { auto mainLayout = new QVBoxLayout(this); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); mOkButton = buttonBox->button(QDialogButtonBox::Ok); mOkButton->setDefault(true); mOkButton->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Return)); mCheckSelectionTimer = new QTimer(this); mStartSearchTimer = new QTimer(this); QFrame *page = new QFrame(this); mainLayout->addWidget(page); mainLayout->addWidget(buttonBox); mTopLayout = new QVBoxLayout(page); mTopLayout->setContentsMargins(0, 0, 0, 0); mTextLabel = new QLabel(page); mTextLabel->setWordWrap(true); // Setting the size policy is necessary as a workaround for https://issues.kolab.org/issue4429 // and http://bugreports.qt.nokia.com/browse/QTBUG-8740 mTextLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); connect(mTextLabel, &QLabel::linkActivated, this, &KeySelectionDialog::slotStartCertificateManager); mTopLayout->addWidget(mTextLabel); mTextLabel->hide(); QPushButton *const searchExternalPB = new QPushButton(i18nc("@action:button", "Search for &External Certificates"), page); mTopLayout->addWidget(searchExternalPB, 0, Qt::AlignLeft); connect(searchExternalPB, &QAbstractButton::clicked, this, &KeySelectionDialog::slotStartSearchForExternalCertificates); if (initialQuery.isEmpty()) { searchExternalPB->hide(); } auto hlay = new QHBoxLayout(); mTopLayout->addLayout(hlay); auto le = new QLineEdit(page); le->setClearButtonEnabled(true); le->setText(initialQuery); QLabel *lbSearchFor = new QLabel(i18nc("@label:textbox", "&Search for:"), page); lbSearchFor->setBuddy(le); hlay->addWidget(lbSearchFor); hlay->addWidget(le, 1); le->setFocus(); connect(le, &QLineEdit::textChanged, this, [this](const QString &s) { slotSearch(s); }); connect(mStartSearchTimer, &QTimer::timeout, this, &KeySelectionDialog::slotFilter); mKeyListView = new KeyListView(new ColumnStrategy(mKeyUsage), nullptr, page); mKeyListView->setObjectName(QLatin1StringView("mKeyListView")); mKeyListView->header()->stretchLastSection(); mKeyListView->setRootIsDecorated(true); mKeyListView->setSortingEnabled(true); mKeyListView->header()->setSortIndicatorShown(true); mKeyListView->header()->setSortIndicator(1, Qt::AscendingOrder); // sort by User ID if (options & ExtendedSelection) { mKeyListView->setSelectionMode(QAbstractItemView::ExtendedSelection); } mTopLayout->addWidget(mKeyListView, 10); if (options & RememberChoice) { mRememberCB = new QCheckBox(i18nc("@option:check", "&Remember choice"), page); mTopLayout->addWidget(mRememberCB); mRememberCB->setWhatsThis( i18n("<qt><p>If you check this box your choice will " "be stored and you will not be asked again." "</p></qt>")); } connect(mCheckSelectionTimer, &QTimer::timeout, this, [this]() { slotCheckSelection(); }); connectSignals(); connect(mKeyListView, &KeyListView::doubleClicked, this, &KeySelectionDialog::slotTryOk); connect(mKeyListView, &KeyListView::contextMenu, this, &KeySelectionDialog::slotRMB); if (options & RereadKeys) { QPushButton *button = new QPushButton(i18nc("@action:button", "&Reread Keys")); buttonBox->addButton(button, QDialogButtonBox::ActionRole); connect(button, &QPushButton::clicked, this, &KeySelectionDialog::slotRereadKeys); } if (options & ExternalCertificateManager) { QPushButton *button = new QPushButton(i18nc("@action:button", "&Start Certificate Manager")); buttonBox->addButton(button, QDialogButtonBox::ActionRole); connect(button, &QPushButton::clicked, this, [this]() { slotStartCertificateManager(); }); } connect(mOkButton, &QPushButton::clicked, this, &KeySelectionDialog::slotOk); connect(buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &KeySelectionDialog::slotCancel); mTopLayout->activate(); if (qApp) { QSize dialogSize(sizeHint()); KConfigGroup dialogConfig(KSharedConfig::openStateConfig(), QStringLiteral("Key Selection Dialog")); dialogSize = dialogConfig.readEntry("Dialog size", dialogSize); const QByteArray headerState = dialogConfig.readEntry("header", QByteArray()); if (!headerState.isEmpty()) { mKeyListView->header()->restoreState(headerState); } resize(dialogSize); } } void KeySelectionDialog::init(bool rememberChoice, bool extendedSelection, const QString &text, const QString &initialQuery) { Options options = {RereadKeys, ExternalCertificateManager}; options.setFlag(ExtendedSelection, extendedSelection); options.setFlag(RememberChoice, rememberChoice); setUpUI(options, initialQuery); setText(text); if (mKeyUsage & OpenPGPKeys) { mOpenPGPBackend = QGpgME::openpgp(); } if (mKeyUsage & SMIMEKeys) { mSMIMEBackend = QGpgME::smime(); } slotRereadKeys(); } KeySelectionDialog::~KeySelectionDialog() { disconnectSignals(); KConfigGroup dialogConfig(KSharedConfig::openStateConfig(), QStringLiteral("Key Selection Dialog")); dialogConfig.writeEntry("Dialog size", size()); dialogConfig.writeEntry("header", mKeyListView->header()->saveState()); dialogConfig.sync(); } void KeySelectionDialog::setText(const QString &text) { mTextLabel->setText(text); mTextLabel->setVisible(!text.isEmpty()); } void KeySelectionDialog::setKeys(const std::vector<GpgME::Key> &keys) { for (const GpgME::Key &key : keys) { mKeyListView->slotAddKey(key); } } void KeySelectionDialog::connectSignals() { if (mKeyListView->isMultiSelection()) { connect(mKeyListView, &QTreeWidget::itemSelectionChanged, this, &KeySelectionDialog::slotSelectionChanged); } else { connect(mKeyListView, qOverload<KeyListViewItem *>(&KeyListView::selectionChanged), this, qOverload<KeyListViewItem *>(&KeySelectionDialog::slotCheckSelection)); } } void KeySelectionDialog::disconnectSignals() { if (mKeyListView->isMultiSelection()) { disconnect(mKeyListView, &QTreeWidget::itemSelectionChanged, this, &KeySelectionDialog::slotSelectionChanged); } else { disconnect(mKeyListView, qOverload<KeyListViewItem *>(&KeyListView::selectionChanged), this, qOverload<KeyListViewItem *>(&KeySelectionDialog::slotCheckSelection)); } } const GpgME::Key &KeySelectionDialog::selectedKey() const { static const GpgME::Key null = GpgME::Key::null; if (mKeyListView->isMultiSelection() || !mKeyListView->selectedItem()) { return null; } return mKeyListView->selectedItem()->key(); } QString KeySelectionDialog::fingerprint() const { return QLatin1StringView(selectedKey().primaryFingerprint()); } QStringList KeySelectionDialog::fingerprints() const { QStringList result; for (auto it = mSelectedKeys.begin(); it != mSelectedKeys.end(); ++it) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1StringView(fpr)); } } return result; } QStringList KeySelectionDialog::pgpKeyFingerprints() const { QStringList result; for (auto it = mSelectedKeys.begin(); it != mSelectedKeys.end(); ++it) { if (it->protocol() == GpgME::OpenPGP) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1StringView(fpr)); } } } return result; } QStringList KeySelectionDialog::smimeFingerprints() const { QStringList result; for (auto it = mSelectedKeys.begin(); it != mSelectedKeys.end(); ++it) { if (it->protocol() == GpgME::CMS) { if (const char *fpr = it->primaryFingerprint()) { result.push_back(QLatin1StringView(fpr)); } } } return result; } void KeySelectionDialog::slotRereadKeys() { mKeyListView->clear(); mListJobCount = 0; mTruncated = 0; mSavedOffsetY = mKeyListView->verticalScrollBar()->value(); disconnectSignals(); mKeyListView->setEnabled(false); // FIXME: save current selection if (mOpenPGPBackend) { startKeyListJobForBackend(mOpenPGPBackend, std::vector<GpgME::Key>(), false /*non-validating*/); } if (mSMIMEBackend) { startKeyListJobForBackend(mSMIMEBackend, std::vector<GpgME::Key>(), false /*non-validating*/); } if (mListJobCount == 0) { mKeyListView->setEnabled(true); KMessageBox::information(this, i18n("No backends found for listing keys. " "Check your installation."), i18nc("@title:window", "Key Listing Failed")); connectSignals(); } } void KeySelectionDialog::slotStartCertificateManager(const QString &query) { QStringList args; if (!query.isEmpty()) { args << QStringLiteral("--search") << query; } const QString exec = QStandardPaths::findExecutable(QStringLiteral("kleopatra")); if (exec.isEmpty()) { qCWarning(KLEO_UI_LOG) << "Could not find kleopatra executable in PATH"; KMessageBox::error(this, i18n("Could not start certificate manager; " "please check your installation."), i18nc("@title:window", "Certificate Manager Error")); } else { QProcess::startDetached(QStringLiteral("kleopatra"), args); qCDebug(KLEO_UI_LOG) << "\nslotStartCertManager(): certificate manager started."; } } #ifndef __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ #define __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ static void showKeyListError(QWidget *parent, const GpgME::Error &err) { Q_ASSERT(err); const QString msg = i18n( "<qt><p>An error occurred while fetching " "the keys from the backend:</p>" "<p><b>%1</b></p></qt>", Formatting::errorAsString(err)); KMessageBox::error(parent, msg, i18nc("@title:window", "Key Listing Failed")); } #endif // __KLEO_UI_SHOW_KEY_LIST_ERROR_H__ namespace { struct ExtractFingerprint { QString operator()(const GpgME::Key &key) { return QLatin1StringView(key.primaryFingerprint()); } }; } void KeySelectionDialog::startKeyListJobForBackend(const QGpgME::Protocol *backend, const std::vector<GpgME::Key> &keys, bool validate) { Q_ASSERT(backend); QGpgME::KeyListJob *job = backend->keyListJob(false, false, validate); // local, w/o sigs, validation as given if (!job) { return; } connect(job, &QGpgME::KeyListJob::result, this, &KeySelectionDialog::slotKeyListResult); if (validate) { connect(job, &QGpgME::KeyListJob::nextKey, mKeyListView, &KeyListView::slotRefreshKey); } else { connect(job, &QGpgME::KeyListJob::nextKey, mKeyListView, &KeyListView::slotAddKey); } QStringList fprs; std::transform(keys.begin(), keys.end(), std::back_inserter(fprs), ExtractFingerprint()); const GpgME::Error err = job->start(fprs, mKeyUsage & SecretKeys && !(mKeyUsage & PublicKeys)); if (err) { return showKeyListError(this, err); } #ifndef LIBKLEO_NO_PROGRESSDIALOG // FIXME: create a MultiProgressDialog: (void)new ProgressDialog(job, validate ? i18n("Checking selected keys...") : i18n("Fetching keys..."), this); #endif ++mListJobCount; } static void selectKeys(KeyListView *klv, const std::vector<GpgME::Key> &selectedKeys) { klv->clearSelection(); if (selectedKeys.empty()) { return; } for (auto it = selectedKeys.begin(); it != selectedKeys.end(); ++it) { if (KeyListViewItem *item = klv->itemByFingerprint(it->primaryFingerprint())) { item->setSelected(true); } } } void KeySelectionDialog::slotKeyListResult(const GpgME::KeyListResult &res) { if (res.error()) { showKeyListError(this, res.error()); } else if (res.isTruncated()) { ++mTruncated; } if (--mListJobCount > 0) { return; // not yet finished... } if (mTruncated > 0) { KMessageBox::information(this, i18np("<qt>One backend returned truncated output.<p>" "Not all available keys are shown</p></qt>", "<qt>%1 backends returned truncated output.<p>" "Not all available keys are shown</p></qt>", mTruncated), i18n("Key List Result")); } mKeyListView->flushKeys(); mKeyListView->setEnabled(true); mListJobCount = mTruncated = 0; mKeysToCheck.clear(); selectKeys(mKeyListView, mSelectedKeys); slotFilter(); connectSignals(); slotSelectionChanged(); // restore the saved position of the contents mKeyListView->verticalScrollBar()->setValue(mSavedOffsetY); mSavedOffsetY = 0; } void KeySelectionDialog::slotSelectionChanged() { qCDebug(KLEO_UI_LOG) << "KeySelectionDialog::slotSelectionChanged()"; // (re)start the check selection timer. Checking the selection is delayed // because else drag-selection doesn't work very good (checking key trust // is slow). mCheckSelectionTimer->start(sCheckSelectionDelay); } namespace { struct AlreadyChecked { bool operator()(const GpgME::Key &key) const { return key.keyListMode() & GpgME::Validate; } }; } void KeySelectionDialog::slotCheckSelection(KeyListViewItem *item) { qCDebug(KLEO_UI_LOG) << "KeySelectionDialog::slotCheckSelection()"; mCheckSelectionTimer->stop(); mSelectedKeys.clear(); if (!mKeyListView->isMultiSelection()) { if (item) { mSelectedKeys.push_back(item->key()); } } for (KeyListViewItem *it = mKeyListView->firstChild(); it; it = it->nextSibling()) { if (it->isSelected()) { mSelectedKeys.push_back(it->key()); } } mKeysToCheck.clear(); std::remove_copy_if(mSelectedKeys.begin(), mSelectedKeys.end(), std::back_inserter(mKeysToCheck), AlreadyChecked()); if (mKeysToCheck.empty()) { mOkButton->setEnabled(!mSelectedKeys.empty() && checkKeyUsage(mSelectedKeys, mKeyUsage)); return; } // performed all fast checks - now for validating key listing: startValidatingKeyListing(); } void KeySelectionDialog::startValidatingKeyListing() { if (mKeysToCheck.empty()) { return; } mListJobCount = 0; mTruncated = 0; mSavedOffsetY = mKeyListView->verticalScrollBar()->value(); disconnectSignals(); mKeyListView->setEnabled(false); std::vector<GpgME::Key> smime; std::vector<GpgME::Key> openpgp; for (std::vector<GpgME::Key>::const_iterator it = mKeysToCheck.begin(); it != mKeysToCheck.end(); ++it) { if (it->protocol() == GpgME::OpenPGP) { openpgp.push_back(*it); } else { smime.push_back(*it); } } if (!openpgp.empty()) { Q_ASSERT(mOpenPGPBackend); startKeyListJobForBackend(mOpenPGPBackend, openpgp, true /*validate*/); } if (!smime.empty()) { Q_ASSERT(mSMIMEBackend); startKeyListJobForBackend(mSMIMEBackend, smime, true /*validate*/); } Q_ASSERT(mListJobCount > 0); } bool KeySelectionDialog::rememberSelection() const { return mRememberCB && mRememberCB->isChecked(); } void KeySelectionDialog::slotRMB(KeyListViewItem *item, const QPoint &p) { if (!item) { return; } mCurrentContextMenuItem = item; QMenu menu; menu.addAction(i18n("Recheck Key"), this, &KeySelectionDialog::slotRecheckKey); menu.exec(p); } void KeySelectionDialog::slotRecheckKey() { if (!mCurrentContextMenuItem || mCurrentContextMenuItem->key().isNull()) { return; } mKeysToCheck.clear(); mKeysToCheck.push_back(mCurrentContextMenuItem->key()); } void KeySelectionDialog::slotTryOk() { if (!mSelectedKeys.empty() && checkKeyUsage(mSelectedKeys, mKeyUsage)) { slotOk(); } } void KeySelectionDialog::slotOk() { if (mCheckSelectionTimer->isActive()) { slotCheckSelection(); } #if 0 // Laurent I don't understand why we returns here. // button could be disabled again after checking the selected key1 if (!mSelectedKeys.empty() && checkKeyUsage(mSelectedKeys, mKeyUsage)) { return; } #endif mStartSearchTimer->stop(); accept(); } void KeySelectionDialog::slotCancel() { mCheckSelectionTimer->stop(); mStartSearchTimer->stop(); reject(); } void KeySelectionDialog::slotSearch(const QString &text) { mSearchText = text.trimmed().toUpper(); slotSearch(); } void KeySelectionDialog::slotSearch() { mStartSearchTimer->setSingleShot(true); mStartSearchTimer->start(sCheckSelectionDelay); } void KeySelectionDialog::slotFilter() { if (mSearchText.isEmpty()) { showAllItems(); return; } // OK, so we need to filter: QRegularExpression keyIdRegExp(QRegularExpression::anchoredPattern(QLatin1StringView("(?:0x)?[A-F0-9]{1,16}")), QRegularExpression::CaseInsensitiveOption); if (keyIdRegExp.match(mSearchText).hasMatch()) { if (mSearchText.startsWith(QLatin1StringView("0X"))) { // search for keyID only: filterByKeyID(mSearchText.mid(2)); } else { // search for UID and keyID: filterByKeyIDOrUID(mSearchText); } } else { // search in UID: filterByUID(mSearchText); } } void KeySelectionDialog::filterByKeyID(const QString &keyID) { Q_ASSERT(keyID.length() <= 16); Q_ASSERT(!keyID.isEmpty()); // regexp in slotFilter should prevent these if (keyID.isEmpty()) { showAllItems(); } else { for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(!item->text(0).remove(u' ').toUpper().startsWith(keyID)); } } } static bool anyUIDMatches(const KeyListViewItem *item, const QRegularExpression &rx) { if (!item) { return false; } const std::vector<GpgME::UserID> uids = item->key().userIDs(); for (auto it = uids.begin(); it != uids.end(); ++it) { if (it->id() && rx.match(QString::fromUtf8(it->id())).hasMatch()) { return true; } } return false; } void KeySelectionDialog::filterByKeyIDOrUID(const QString &str) { Q_ASSERT(!str.isEmpty()); // match beginnings of words: QRegularExpression rx(QLatin1StringView("\\b") + QRegularExpression::escape(str), QRegularExpression::CaseInsensitiveOption); for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(!item->text(0).remove(u' ').toUpper().startsWith(str) && !anyUIDMatches(item, rx)); } } void KeySelectionDialog::filterByUID(const QString &str) { Q_ASSERT(!str.isEmpty()); // match beginnings of words: QRegularExpression rx(QLatin1StringView("\\b") + QRegularExpression::escape(str), QRegularExpression::CaseInsensitiveOption); for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(!anyUIDMatches(item, rx)); } } void KeySelectionDialog::showAllItems() { for (KeyListViewItem *item = mKeyListView->firstChild(); item; item = item->nextSibling()) { item->setHidden(false); } } #include "moc_keyselectiondialog.cpp" diff --git a/src/utils/formatting.cpp b/src/utils/formatting.cpp index 977e117a..9c60945b 100644 --- a/src/utils/formatting.cpp +++ b/src/utils/formatting.cpp @@ -1,1750 +1,1760 @@ /* -*- 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 <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #include <config-libkleo.h> #include "formatting.h" #include "algorithm.h" #include "compat.h" #include "compliance.h" #include "cryptoconfig.h" #include "gnupg.h" #include "keyhelpers.h" #include <libkleo/dn.h> #include <libkleo/keycache.h> #include <libkleo/keygroup.h> #include <libkleo_debug.h> #include <KEmailAddress> #include <KLocalizedString> #include <QGpgME/CryptoConfig> +#include <QGpgME/DN> #include <QGpgME/Protocol> #include <QDateTime> #include <QIcon> #include <QLocale> #include <QRegularExpression> #include <QString> #include <gpgme++/importresult.h> #include <gpgme++/key.h> #include <gpg-error.h> using namespace GpgME; using namespace Kleo; using namespace Qt::Literals::StringLiterals; namespace { QIcon iconForValidityAndCompliance(UserID::Validity validity, bool isCompliant) { switch (validity) { case UserID::Ultimate: case UserID::Full: case UserID::Marginal: return isCompliant ? Formatting::successIcon() : Formatting::infoIcon(); case UserID::Never: return Formatting::errorIcon(); case UserID::Undefined: case UserID::Unknown: default: return Formatting::infoIcon(); } } QIcon iconForValidity(const UserID &userId) { const bool keyIsCompliant = !DeVSCompliance::isActive() || // (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(userId.parent())); return iconForValidityAndCompliance(userId.validity(), keyIsCompliant); } } QIcon Formatting::IconProvider::icon(const GpgME::Key &key) const { return icon(key.userID(0)); } QIcon Formatting::IconProvider::icon(const GpgME::UserID &userID) const { if (usage.canEncrypt() && !Kleo::canBeUsedForEncryption(userID.parent())) { return Formatting::errorIcon(); } if (usage.canSign() && !Kleo::canBeUsedForSigning(userID.parent())) { return Formatting::errorIcon(); } if (userID.parent().isBad() || userID.isBad()) { return Formatting::errorIcon(); } if (Kleo::isRevokedOrExpired(userID)) { return Formatting::errorIcon(); } return iconForValidity(userID); } QIcon Formatting::IconProvider::icon(const KeyGroup &group) const { if (usage.canEncrypt() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForEncryption)) { return Formatting::errorIcon(); } if (usage.canSign() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForSigning)) { return Formatting::errorIcon(); } return validityIcon(group); } QIcon Formatting::successIcon() { return QIcon::fromTheme(QStringLiteral("emblem-success")); } QIcon Formatting::infoIcon() { return QIcon::fromTheme(QStringLiteral("emblem-information")); } QIcon Formatting::questionIcon() { return QIcon::fromTheme(QStringLiteral("emblem-question")); } QIcon Formatting::unavailableIcon() { return QIcon::fromTheme(QStringLiteral("emblem-unavailable")); } QIcon Formatting::warningIcon() { return QIcon::fromTheme(QStringLiteral("emblem-warning")); } QIcon Formatting::errorIcon() { return QIcon::fromTheme(QStringLiteral("emblem-error")); } // // Name // QString Formatting::prettyName(int proto, const char *id, const char *name_, const char *comment_) { if (proto == GpgME::OpenPGP) { const QString name = QString::fromUtf8(name_); if (name.isEmpty()) { return QString(); } const QString comment = QString::fromUtf8(comment_); if (comment.isEmpty()) { return name; } return QStringLiteral("%1 (%2)").arg(name, comment); } if (proto == GpgME::CMS) { - const DN subject(id); + const QGpgME::DN subject(id); const QString cn = subject[QStringLiteral("CN")].trimmed(); if (cn.isEmpty()) { + subject.setAttributeOrder(DNAttributes::order()); 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 QGpgME::DN subject(id); const QString cn = subject[QStringLiteral("CN")].trimmed(); if (cn.isEmpty()) { + subject.setAttributeOrder(DN::attributeOrder()); 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(); + return prettyDN(uid.id()); } } QString Formatting::prettyKeyID(const char *id) { if (!id) { return QString(); } return QLatin1StringView("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(); } } +QString Formatting::prettyDN(const char *utf8DN) +{ + QGpgME::DN dn{utf8DN}; + dn.setAttributeOrder(Kleo::DN::attributeOrder()); + return dn.prettyDN(); +} + // // Tooltip // namespace { static QString protect_whitespace(QString s) { static const QLatin1Char SP(' '); static const QLatin1Char NBSP('\xA0'); return s.replace(SP, NBSP); } template<typename T_arg> QString format_row(const QString &field, const T_arg &arg) { return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").arg(protect_whitespace(field), arg); } QString format_row(const QString &field, const QString &arg) { return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").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(), QLatin1StringView(subkey.publicKeyAlgorithmAsString())); } else { return i18n("%1-bit %2", subkey.length(), QLatin1StringView(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(), QLatin1StringView(subkey.publicKeyAlgorithmAsString())); } QString format_keyusage(const Key &key) { QStringList capabilities; if (Kleo::keyHasSign(key)) { if (key.isQualified()) { capabilities.push_back(i18n("Signing (Qualified)")); } else { capabilities.push_back(i18n("Signing")); } } if (Kleo::keyHasEncrypt(key)) { capabilities.push_back(i18n("Encryption")); } if (Kleo::keyHasCertify(key)) { capabilities.push_back(i18n("Certifying User IDs")); } if (Kleo::keyHasAuthenticate(key)) { capabilities.push_back(i18n("SSH Authentication")); } return capabilities.join(QLatin1StringView(", ")); } 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(QLatin1StringView(", ")); } 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 QLatin1StringView("<font color=\"red\">") + txt.toHtmlEscaped() + QLatin1StringView("</font>"); } } static QString toolTipInternal(const GpgME::Key &key, const GpgME::UserID &userID, 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 & Formatting::Validity) { if (key.protocol() == GpgME::OpenPGP || (key.keyListMode() & Validate)) { if (key.isDisabled()) { result = i18n("Disabled"); } else if (userID.isRevoked() || key.isRevoked()) { result = make_red(i18n("Revoked")); } else if (key.isExpired()) { result = make_red(i18n("Expired")); } else if (key.keyListMode() & GpgME::Validate) { if (!userID.isNull()) { if (userID.validity() >= UserID::Validity::Full) { result = i18n("User ID is certified."); const auto compliance = Formatting::complianceStringForUserID(userID); if (!compliance.isEmpty()) { result += QStringLiteral("<br>") + compliance; } } else { result = i18n("User ID is not certified."); } } else { 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 = Formatting::complianceStringForKey(key); if (!compliance.isEmpty()) { result += QStringLiteral("<br>") + 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 == Formatting::Validity) { return result; } result += QLatin1StringView("<table border=\"0\">"); if (key.protocol() == GpgME::CMS) { if (flags & Formatting::SerialNumber) { result += format_row(i18n("Serial number"), key.issuerSerial()); } if (flags & Formatting::Issuer) { result += format_row(i18n("Issuer"), key.issuerName()); } } if (flags & Formatting::UserIDs) { if (userID.isNull()) { const std::vector<UserID> uids = key.userIDs(); if (!uids.empty()) { result += format_row(key.protocol() == GpgME::CMS ? i18n("Subject") : i18n("User ID"), Formatting::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."), Formatting::prettyUserID(*it)); } } } } else { result += format_row(key.protocol() == GpgME::CMS ? i18n("Subject") : i18n("User ID"), Formatting::prettyUserID(userID)); } } if (flags & Formatting::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 & Formatting::CertificateType) { result += format_row(i18n("Type"), format_keytype(key)); } if (flags & Formatting::CertificateUsage) { result += format_row(i18n("Usage"), format_keyusage(key)); } if (flags & Formatting::KeyID) { result += format_row(i18n("Key ID"), QString::fromLatin1(key.keyID())); } if (flags & Formatting::Fingerprint) { result += format_row(i18n("Fingerprint"), key.primaryFingerprint()); } if (flags & Formatting::OwnerTrust) { if (key.protocol() == GpgME::OpenPGP) { result += format_row(i18n("Certification trust"), Formatting::ownerTrustShort(key)); } else if (key.isRoot()) { result += format_row(i18n("Trusted issuer?"), (userID.isNull() ? key.userID(0) : userID).validity() == UserID::Ultimate ? i18n("Yes") : i18n("No")); } } if (flags & Formatting::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 & Formatting::Subkeys) { for (const auto &sub : key.subkeys()) { result += QLatin1StringView("<hr/>"); 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 & Formatting::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 & Formatting::CertificateType) { result += format_row(i18n("Type"), format_subkeytype(sub)); } if (flags & Formatting::CertificateUsage) { result += format_row(i18n("Usage"), format_subkeyusage(sub)); } if (flags & Formatting::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 += QLatin1StringView("</table>"); return result; } QString Formatting::toolTip(const Key &key, int flags) { return toolTipInternal(key, UserID(), flags); } namespace { template<typename Container> 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."); } if (Kleo::any_of(keys, [](const auto &key) { return !key.hasEncrypt(); })) { return i18nc("@info:tooltip", "Some of the certificates in this group cannot be used for encryption. Using this group can lead to unexpected results."); } 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("<p>")); result.push_back(validity.toHtmlEscaped()); result.push_back(QStringLiteral("</p>")); } result.push_back(QStringLiteral("<p>")); result.push_back(i18n("Keys:")); { auto it = keys.cbegin(); for (unsigned int i = 0; i < numKeysForTooltip; ++i, ++it) { result.push_back(QLatin1StringView("<br>") + Formatting::summaryLine(*it).toHtmlEscaped()); } } if (keys.size() > numKeysForTooltip) { result.push_back(QLatin1StringView("<br>") + i18ncp("this follows a list of keys", "and 1 more key", "and %1 more keys", keys.size() - numKeysForTooltip)); } result.push_back(QStringLiteral("</p>")); return result.join(QLatin1Char('\n')); } QString Formatting::toolTip(const UserID &userID, int flags) { return toolTipInternal(userID.parent(), userID, flags); } // // 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<typename T> QString expiration_date_string(const T &tee, const QString &noExpiration) { return tee.neverExpires() ? noExpiration : Formatting::dateString(time_t2date(tee.expirationTime())); } template<typename T> QDate creation_date(const T &tee) { return time_t2date(tee.creationTime()); } template<typename T> 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.isDisabled()) { return i18n("disabled"); } if (subkey.isRevoked()) { return i18n("revoked"); } if (subkey.isExpired()) { return i18n("expired"); } if (subkey.isInvalid()) { return i18n("invalid"); } return i18nc("as in 'this subkey is ok'", "OK"); } QString Formatting::validityShort(const UserID &uid) { if (uid.isRevoked()) { return i18n("revoked"); } if (uid.isInvalid()) { return i18n("invalid"); } switch (uid.validity()) { case UserID::Unknown: return i18nc("unknown trust level", "unknown"); case UserID::Undefined: return i18nc("undefined trust", "undefined"); case UserID::Never: return i18n("untrusted"); case UserID::Marginal: return i18nc("marginal trust", "marginal"); case UserID::Full: return i18nc("full trust", "full"); case UserID::Ultimate: return i18nc("ultimate trust", "ultimate"); } return QString(); } QString Formatting::validityShort(const UserID::Signature &sig) { switch (sig.status()) { case UserID::Signature::NoError: if (!sig.isInvalid()) { /* See RFC 4880 Section 5.2.1 */ switch (sig.certClass()) { case 0x10: /* Generic */ case 0x11: /* Persona */ case 0x12: /* Casual */ case 0x13: /* Positive */ return i18n("valid"); case 0x30: return i18n("revoked"); default: return i18n("class %1", sig.certClass()); } } [[fallthrough]]; // fall through: case UserID::Signature::GeneralError: return i18n("invalid"); case UserID::Signature::SigExpired: return i18n("expired"); case UserID::Signature::KeyExpired: return i18n("certificate expired"); case UserID::Signature::BadSignature: return i18nc("fake/invalid signature", "bad"); case UserID::Signature::NoPublicKey: { /* GnuPG returns the same error for no public key as for expired * or revoked certificates. */ const auto key = KeyCache::instance()->findByKeyIDOrFingerprint(sig.signerKeyID()); if (key.isNull()) { return i18n("no public key"); } else if (key.isDisabled()) { return i18n("key disabled"); } else if (key.isRevoked()) { return i18n("key revoked"); } else if (key.isExpired()) { return i18n("key expired"); } /* can't happen */ return QStringLiteral("unknown"); } } return QString(); } QIcon Formatting::validityIcon(const UserID::Signature &sig) { switch (sig.status()) { case UserID::Signature::NoError: if (!sig.isInvalid()) { /* See RFC 4880 Section 5.2.1 */ switch (sig.certClass()) { case 0x10: /* Generic */ case 0x11: /* Persona */ case 0x12: /* Casual */ case 0x13: /* Positive */ return Formatting::successIcon(); case 0x30: return Formatting::errorIcon(); default: return QIcon(); } } [[fallthrough]]; // fall through: case UserID::Signature::BadSignature: case UserID::Signature::GeneralError: return Formatting::errorIcon(); case UserID::Signature::SigExpired: case UserID::Signature::KeyExpired: return Formatting::infoIcon(); case UserID::Signature::NoPublicKey: return Formatting::questionIcon(); } return QIcon(); } QString Formatting::formatKeyLink(const Key &key) { if (key.isNull()) { return QString(); } return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(QLatin1StringView(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, QLatin1StringView(key.keyID())).simplified(); } QString Formatting::nameAndEmailForSummaryLine(const UserID &id) { Q_ASSERT(!id.isNull()); const QString email = Formatting::prettyEMail(id); const QString name = Formatting::prettyName(id); if (name.isEmpty()) { return email; } else if (email.isEmpty()) { return name; } else { return QStringLiteral("%1 <%2>").arg(name, email); } } QString Formatting::nameAndEmailForSummaryLine(const Key &key) { Q_ASSERT(!key.isNull()); const QString email = Formatting::prettyEMail(key); const QString name = Formatting::prettyName(key); if (name.isEmpty()) { return email; } else if (email.isEmpty()) { return name; } else { return QStringLiteral("%1 <%2>").arg(name, email); } } const char *Formatting::summaryToString(const Signature::Summary summary) { if (summary & Signature::Red) { return "RED"; } if (summary & Signature::Green) { return "GREEN"; } return "YELLOW"; } QString Formatting::signatureToString(const Signature &sig, const Key &key) { if (sig.isNull()) { return QString(); } const bool red = (sig.summary() & Signature::Red); const bool valid = (sig.summary() & Signature::Valid); if (red) { if (key.isNull()) { if (const char *fpr = sig.fingerprint()) { return i18n("Bad signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status())); } else { return i18n("Bad signature by an unknown certificate: %1", Formatting::errorAsString(sig.status())); } } else { return i18n("Bad signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status())); } } else if (valid) { if (key.isNull()) { if (const char *fpr = sig.fingerprint()) { return i18n("Good signature by unknown certificate %1.", QString::fromLatin1(fpr)); } else { return i18n("Good signature by an unknown certificate."); } } else { return i18n("Good signature by %1.", nameAndEmailForSummaryLine(key)); } } else if (key.isNull()) { if (const char *fpr = sig.fingerprint()) { return i18n("Invalid signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status())); } else { return i18n("Invalid signature by an unknown certificate: %1", Formatting::errorAsString(sig.status())); } } else { return i18n("Invalid signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status())); } } // // ImportResult // QString Formatting::importMetaData(const Import &import, const QStringList &ids) { const QString result = importMetaData(import); if (result.isEmpty()) { return QString(); } else { return result + QLatin1Char('\n') + i18n("This certificate was imported from the following sources:") + QLatin1Char('\n') + ids.join(QLatin1Char('\n')); } } QString Formatting::importMetaData(const Import &import) { if (import.isNull()) { return QString(); } if (import.error().isCanceled()) { return i18n("The import of this certificate was canceled."); } if (import.error()) { return i18n("An error occurred importing this certificate: %1", Formatting::errorAsString(import.error())); } const unsigned int status = import.status(); if (status & Import::NewKey) { return (status & Import::ContainedSecretKey) ? i18n("This certificate was new to your keystore. The secret key is available.") : i18n("This certificate is new to your keystore."); } QStringList results; if (status & Import::NewUserIDs) { results.push_back(i18n("New user-ids were added to this certificate by the import.")); } if (status & Import::NewSignatures) { results.push_back(i18n("New signatures were added to this certificate by the import.")); } if (status & Import::NewSubkeys) { results.push_back(i18n("New subkeys were added to this certificate by the import.")); } return results.empty() ? i18n("The import contained no new data for this certificate. It is unchanged.") : results.join(QLatin1Char('\n')); } // // Overview in CertificateDetailsDialog // QString Formatting::formatOverview(const Key &key) { return toolTip(key, AllOptions); } QString Formatting::usageString(const Subkey &sub) { QStringList usageStrings; if (sub.canCertify()) { usageStrings << i18n("Certify"); } if (sub.canSign()) { usageStrings << i18n("Sign"); } if (sub.canEncrypt()) { usageStrings << i18n("Encrypt"); } if (sub.canAuthenticate()) { usageStrings << i18n("Authenticate"); } if (sub.canRenc()) { usageStrings << i18nc("Means 'Additional Decryption Subkey'; Don't try translating that, though.", "ADSK"); } return usageStrings.join(QLatin1StringView(", ")); } QString Formatting::summaryLine(const UserID &id) { return i18nc("name <email> (validity, protocol, creation date)", "%1 (%2, %3, created: %4)", nameAndEmailForSummaryLine(id), Formatting::complianceStringShort(id), displayName(id.parent().protocol()), Formatting::creationDateString(id.parent())); } QString Formatting::summaryLine(const Key &key) { return nameAndEmailForSummaryLine(key) + QLatin1Char(' ') + i18nc("(validity, protocol, creation date)", "(%1, %2, created: %3)", Formatting::complianceStringShort(key), displayName(key.protocol()), Formatting::creationDateString(key)); } QString Formatting::summaryLine(const KeyGroup &group) { switch (group.source()) { case KeyGroup::ApplicationConfig: case KeyGroup::GnuPGConfig: return i18ncp("name of group of keys (n key(s), validity)", "%2 (1 key, %3)", "%2 (%1 keys, %3)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); case KeyGroup::Tags: return i18ncp("name of group of keys (n key(s), validity, tag)", "%2 (1 key, %3, tag)", "%2 (%1 keys, %3, tag)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); default: return i18ncp("name of group of keys (n key(s), validity, group ...)", "%2 (1 key, %3, unknown origin)", "%2 (%1 keys, %3, unknown origin)", group.keys().size(), group.name(), Formatting::complianceStringShort(group)); } } // Icon for certificate selection indication QIcon Formatting::iconForUid(const UserID &uid) { if (Kleo::isRevokedOrExpired(uid)) { return Formatting::errorIcon(); } return iconForValidity(uid); } QString Formatting::validity(const UserID &uid) { switch (uid.validity()) { case UserID::Ultimate: return i18n("The certificate is marked as your own."); case UserID::Full: return i18n("The certificate belongs to this recipient."); case UserID::Marginal: return i18n("The trust model indicates marginally that the certificate belongs to this recipient."); case UserID::Never: return i18n("This certificate should not be used."); case UserID::Undefined: case UserID::Unknown: default: return i18n("There is no indication that this certificate belongs to this recipient."); } } QString Formatting::validity(const KeyGroup &group) { if (group.isNull()) { return QString(); } const KeyGroup::Keys &keys = group.keys(); if (keys.size() == 0) { return i18n("This group does not contain any keys."); } return getValidityStatement(keys); } namespace { template<typename Container> 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<int>(validity, minimalValidityOfNotRevokedUserIDs(key)); }); return minValidity <= UserID::Ultimate ? static_cast<UserID::Validity>(minValidity) : UserID::Unknown; } template<typename Container> bool allKeysAreCompliant(const Container &keys) { if (!DeVSCompliance::isActive()) { return true; } if (!DeVSCompliance::isCompliant()) { return false; } return Kleo::all_of(keys, DeVSCompliance::keyIsCompliant); } } QIcon Formatting::validityIcon(const KeyGroup &group) { if (Kleo::any_of(group.keys(), std::mem_fn(&Key::isBad))) { return Formatting::errorIcon(); } return iconForValidityAndCompliance(minimalValidity(group.keys()), allKeysAreCompliant(group.keys())); } QString Formatting::complianceMode() { const auto complianceValue = getCryptoConfigStringValue("gpg", "compliance"); return complianceValue == QLatin1StringView("gnupg") ? QString() : complianceValue; } 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::complianceStringForUserID(const GpgME::UserID &userID) { // There will likely be more in the future for other institutions // for now we only have DE-VS if (DeVSCompliance::isCompliant()) { return isRemoteKey(userID.parent()) // ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown") : DeVSCompliance::name(DeVSCompliance::userIDIsCompliant(userID)); } return QString(); } QString Formatting::complianceStringShort(const GpgME::UserID &id) { if (DeVSCompliance::isCompliant() && DeVSCompliance::userIDIsCompliant(id)) { return QStringLiteral("★ ") + DeVSCompliance::name(true); } const bool keyValidityChecked = (id.parent().keyListMode() & GpgME::Validate); if (keyValidityChecked && id.validity() >= UserID::Full) { return i18nc("As in 'this user ID is valid.'", "certified"); } if (id.parent().isDisabled()) { return i18n("disabled"); } if (id.parent().isRevoked() || id.isRevoked()) { return i18n("revoked"); } if (id.parent().isExpired() || isExpired(id)) { return i18n("expired"); } if (id.parent().isInvalid() || id.isInvalid()) { return i18n("invalid"); } if (keyValidityChecked) { return i18nc("As in 'this user ID is not certified'", "not certified"); } return i18nc("The validity of this user ID has not been/could not be checked", "not checked"); } QString Formatting::complianceStringShort(const GpgME::Key &key) { if (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(key)) { return QStringLiteral("★ ") + DeVSCompliance::name(true); } const bool keyValidityChecked = (key.keyListMode() & GpgME::Validate); if (key.isDisabled()) { return i18n("disabled"); } if (key.isRevoked()) { return i18n("revoked"); } if (key.isExpired()) { return i18n("expired"); } if (key.isInvalid()) { return i18n("invalid"); } if (keyValidityChecked && Kleo::allUserIDsHaveFullValidity(key)) { return i18nc("As in all user IDs are valid.", "certified"); } 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(); if (ret.size() == 64) { // looks like a V5 fingerprint; format the first 25 bytes as 10 groups of 5 hex characters ret.truncate(50); return ret.replace(QRegularExpression(QStringLiteral("(.....)")), QStringLiteral("\\1 ")).trimmed(); } ret = ret.replace(QRegularExpression(QStringLiteral("(....)")), QStringLiteral("\\1 ")).trimmed(); // For the standard 10 group V4 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("(?:(.)(.)(.)(.))")}; static const QRegularExpression groupOfFiveRegExp{QStringLiteral("(?:(.)(.)(.)(.)(.))")}; QString ret; ret = QString::fromLatin1(id); if (ret.size() == 64) { ret.truncate(50); return ret.replace(groupOfFiveRegExp, QStringLiteral("\\1 \\2 \\3 \\4 \\5, ")).chopped(2); } 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 {}; } } namespace { QString formatTrustScope(const char *trustScope) { static const QRegularExpression escapedNonAlphaNum{QStringLiteral(R"(\\([^0-9A-Za-z]))")}; const auto scopeRegExp = QString::fromUtf8(trustScope); if (scopeRegExp.startsWith(u"<[^>]+[@.]") && scopeRegExp.endsWith(u">$")) { // looks like a trust scope regular expression created by gpg auto domain = scopeRegExp.mid(10, scopeRegExp.size() - 10 - 2); domain.replace(escapedNonAlphaNum, QStringLiteral(R"(\1)")); return domain; } return scopeRegExp; } } QString Formatting::trustSignatureDomain(const GpgME::UserID::Signature &sig) { return formatTrustScope(sig.trustScope()); } QString Formatting::trustSignature(const GpgME::UserID::Signature &sig) { switch (sig.trustValue()) { case TrustSignatureTrust::Partial: return i18nc("Certifies this key as partially trusted introducer for 'domain name'.", "Certifies this key as partially trusted introducer for '%1'.", trustSignatureDomain(sig)); case TrustSignatureTrust::Complete: return i18nc("Certifies this key as fully trusted introducer for 'domain name'.", "Certifies this key as fully trusted introducer for '%1'.", trustSignatureDomain(sig)); default: return {}; } } QString Formatting::errorAsString(const GpgME::Error &error) { #ifdef Q_OS_WIN // On Windows, we set GpgME resp. libgpg-error to return (translated) error messages as UTF-8 #if GPGMEPP_ERROR_HAS_ASSTDSTRING const std::string s = error.asStdString(); qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1); qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s; qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray::fromStdString(s).toPercentEncoding(); return QString::fromStdString(s); #else const char *s = error.asString(); qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1); qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s; qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray{s}.toPercentEncoding(); return QString::fromUtf8(s); #endif #else #if GPGMEPP_ERROR_HAS_ASSTDSTRING const std::string s = error.asStdString(); return QString::fromLocal8Bit(QByteArrayView{s.data(), qsizetype(s.size())}); #else return QString::fromLocal8Bit(error.asString()); #endif #endif } QString Formatting::prettyAlgorithmName(const std::string &algorithm) { static const std::map<std::string, QString> displayNames = { {"brainpoolP256r1", i18nc("@info", "ECC (Brainpool P-256)")}, {"brainpoolP384r1", i18nc("@info", "ECC (Brainpool P-384)")}, {"brainpoolP512r1", i18nc("@info", "ECC (Brainpool P-512)")}, {"curve25519", i18nc("@info", "ECC (Curve25519)")}, {"curve448", i18nc("@info", "ECC (Curve448)")}, {"ed25519", i18nc("@info", "ECC (Ed25519)")}, {"ed448", i18nc("@info", "ECC (Ed448)")}, {"cv25519", i18nc("@info", "ECC (Cv25519)")}, {"cv448", i18nc("@info", "ECC (Cv448)")}, {"nistp256", i18nc("@info", "ECC (NIST P-256)")}, {"nistp384", i18nc("@info", "ECC (NIST P-384)")}, {"nistp521", i18nc("@info", "ECC (NIST P-521)")}, {"rsa1024", i18nc("@info", "RSA 1024")}, {"rsa2048", i18nc("@info", "RSA 2048")}, {"rsa3072", i18nc("@info", "RSA 3072")}, {"rsa4096", i18nc("@info", "RSA 4096")}, {"dsa1024", i18nc("@info", "DSA 1024")}, {"dsa2048", i18nc("@info", "DSA 2048")}, {"elg1024", i18nc("@info", "Elgamal 1024")}, {"elg2048", i18nc("@info", "Elgamal 2048")}, {"elg3072", i18nc("@info", "Elgamal 3072")}, {"elg4096", i18nc("@info", "Elgamal 4096")}, }; const auto it = displayNames.find(algorithm); return (it != displayNames.end()) ? it->second : QString::fromStdString(algorithm); } static QString formatValidSignatureWithTrustLevel(const GpgME::UserID &id) { if (id.isNull()) { return QString(); } switch (id.validity()) { case GpgME::UserID::Marginal: return i18n("The signature is valid but the trust in the certificate's validity is only marginal."); case GpgME::UserID::Full: return i18n("The signature is valid and the certificate's validity is fully trusted."); case GpgME::UserID::Ultimate: return i18n("The signature is valid and the certificate's validity is ultimately trusted."); case GpgME::UserID::Never: return i18n("The signature is valid but the certificate's validity is <em>not trusted</em>."); case GpgME::UserID::Unknown: return i18n("The signature is valid but the certificate's validity is unknown."); case GpgME::UserID::Undefined: default: return i18n("The signature is valid but the certificate's validity is undefined."); } } static QString renderKeyLink(const QString &fpr, const QString &text) { return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(fpr, text.toHtmlEscaped()); } static QString renderKey(const GpgME::Key &key) { if (key.isNull()) { return i18n("Unknown certificate"); } return renderKeyLink(QLatin1StringView(key.primaryFingerprint()), i18nc("User ID (Key ID)", "%1 (%2)", Formatting::prettyNameAndEMail(key), Formatting::prettyID(key.subkey(0).keyID()))); } static QString formatSigningInformation(const GpgME::Signature &sig, const GpgME::Key &key) { if (sig.isNull()) { return QString(); } QString text; const QDateTime dt = sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime(); if (key.isNull()) { const auto id = QStringLiteral("<br/><a href='certificate:%1'>%2</a>").arg(QString::fromLatin1(sig.fingerprint()), Formatting::prettyID(sig.fingerprint())); if (dt.isValid()) { return i18nc("1 is a date", "Signature created on %1 using an unknown certificate with fingerprint %2", QLocale().toString(dt, QLocale::ShortFormat), id); } return i18n("Signature created using an unknown certificate with fingerprint %1", id); } if (dt.isValid()) { text += i18nc("1 is a date", "Signature created on %1 with certificate: %2", QLocale().toString(dt, QLocale::ShortFormat), renderKey(key)); } else { text += i18n("Signature created with certificate: %1", renderKey(key)); } if (Kleo::DeVSCompliance::isCompliant() && ((sig.summary() & GpgME::Signature::Valid) || (sig.summary() & GpgME::Signature::Green))) { text += (QStringLiteral("<br/>") + (sig.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The signature is %1", Kleo::DeVSCompliance::name(true)) : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant", "The signature <b>is not</b> %1.", Kleo::DeVSCompliance::name(true)))); } return text; } static QString signatureSummaryToString(GpgME::Signature::Summary summary) { if (summary & GpgME::Signature::None) { return i18n("Error: Signature not verified"); } else if ((summary & GpgME::Signature::Valid) || (summary & GpgME::Signature::Green)) { return i18n("Good signature"); } else if (summary & GpgME::Signature::KeyRevoked) { return i18n("Signing certificate was revoked"); } else if (summary & GpgME::Signature::KeyExpired) { return i18n("Signing certificate is expired"); } else if (summary & GpgME::Signature::KeyMissing) { return i18n("Certificate is not available"); } else if (summary & GpgME::Signature::SigExpired) { return i18n("Signature expired"); } else if (summary & GpgME::Signature::CrlMissing) { return i18n("CRL missing"); } else if (summary & GpgME::Signature::CrlTooOld) { return i18n("CRL too old"); } else if (summary & GpgME::Signature::BadPolicy) { return i18n("Bad policy"); } else if (summary & GpgME::Signature::SysError) { return i18n("System error"); // ### retrieve system error details? } else if (summary & GpgME::Signature::Red) { return i18n("Bad signature"); } return QString(); } static QLatin1StringView stripAngleBrackets(const QLatin1StringView &str) { if (str.isEmpty()) { return str; } if (str[0] == '<' && str[str.size() - 1] == '>') { return str.mid(1, str.size() - 2); } return str; } QString Formatting::email(const GpgME::UserID &uid) { if (uid.parent().protocol() == GpgME::OpenPGP) { const QLatin1StringView email(uid.email()); if (!email.isEmpty()) { return stripAngleBrackets(email).toString(); } return {}; } Q_ASSERT(uid.parent().protocol() == GpgME::CMS); const QLatin1StringView id(uid.id()); if (!id.isEmpty()) { if (id[0] == '<') { return stripAngleBrackets(id).toString(); } return Kleo::DN(id)[QStringLiteral("EMAIL")].trimmed(); } return {}; } static GpgME::UserID findUserIDByMailbox(const GpgME::Key &key, const QString &email) { const auto userIDs{key.userIDs()}; for (const GpgME::UserID &id : userIDs) { if (Formatting::email(id).compare(email, Qt::CaseInsensitive)) { return id; } } return {}; } QString Kleo::Formatting::prettySignature(const GpgME::Signature &sig, const QString &sender) { if (sig.isNull()) { return QString(); } const GpgME::Key key = Kleo::KeyCache::instance()->findSigner(sig); const QString text = formatSigningInformation(sig, key) + QLatin1StringView("<br/>"); // Green if (sig.summary() & GpgME::Signature::Valid) { GpgME::UserID id = findUserIDByMailbox(key, sender); if (id.isNull()) { for (int i = 0, count = key.userIDs().size(); i < count; i++) { id = key.userID(i); if (!id.isNull()) { break; } } } return text + formatValidSignatureWithTrustLevel(!id.isNull() ? id : key.userID(0)); } // Red if ((sig.summary() & GpgME::Signature::Red)) { const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); if (sig.summary() & GpgME::Signature::SysError) { return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status())); } return ret; } // Key missing if ((sig.summary() & GpgME::Signature::KeyMissing)) { return text + i18n("You can search the certificate on a keyserver or import it from a file."); } // Yellow if ((sig.validity() & GpgME::Signature::Validity::Undefined) // || (sig.validity() & GpgME::Signature::Validity::Unknown) // || (sig.summary() == GpgME::Signature::Summary::None)) { return text + (key.protocol() == GpgME::OpenPGP ? i18n("The used key is not certified by you or any trusted person.") : i18n("The used certificate is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown.")); } // Catch all fall through const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary())); if (sig.summary() & GpgME::Signature::SysError) { return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status())); } return ret; } diff --git a/src/utils/formatting.h b/src/utils/formatting.h index 0c027ec9..645c3371 100644 --- a/src/utils/formatting.h +++ b/src/utils/formatting.h @@ -1,240 +1,242 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/formatting.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "keyusage.h" #include "kleo_export.h" #include <QStringList> #include <gpgme++/key.h> class QString; class QDate; class QIcon; namespace GpgME { class Error; class Import; } namespace Kleo { class KeyGroup; namespace Formatting { class KLEO_EXPORT IconProvider { public: inline explicit IconProvider(KeyUsage::Flags requiredUsages) : usage{requiredUsages} { } QIcon icon(const GpgME::Key &key) const; QIcon icon(const KeyGroup &group) const; QIcon icon(const GpgME::UserID &userID) const; private: KeyUsage usage; }; KLEO_EXPORT QIcon successIcon(); KLEO_EXPORT QIcon infoIcon(); KLEO_EXPORT QIcon questionIcon(); KLEO_EXPORT QIcon unavailableIcon(); KLEO_EXPORT QIcon warningIcon(); KLEO_EXPORT QIcon errorIcon(); KLEO_EXPORT QString prettyNameAndEMail(int proto, const char *id, const char *name, const char *email, const char *comment = nullptr); KLEO_EXPORT QString prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment = {}); KLEO_EXPORT QString prettyNameAndEMail(const GpgME::Key &key); KLEO_EXPORT QString prettyNameAndEMail(const GpgME::UserID &key); KLEO_EXPORT QString prettyUserID(const GpgME::UserID &uid); KLEO_EXPORT QString prettyKeyID(const char *id); KLEO_EXPORT QString prettyName(int proto, const char *id, const char *name, const char *comment = nullptr); KLEO_EXPORT QString prettyName(const GpgME::Key &key); KLEO_EXPORT QString prettyName(const GpgME::UserID &uid); KLEO_EXPORT QString prettyName(const GpgME::UserID::Signature &sig); KLEO_EXPORT QString prettyEMail(const char *email, const char *id); KLEO_EXPORT QString prettyEMail(const GpgME::Key &key); KLEO_EXPORT QString prettyEMail(const GpgME::UserID &uid); KLEO_EXPORT QString prettyEMail(const GpgME::UserID::Signature &sig); +KLEO_EXPORT QString prettyDN(const char *utf8DN); + /* Formats a fingerprint or keyid into groups of four */ KLEO_EXPORT QString prettyID(const char *id); KLEO_EXPORT QString accessibleHexID(const char *id); /** * Formats a signature from a VerificationResult. * * @param signature The signature to display. * @param sender The sender of the signature, if multiple UserIds are found, this will be the displayed one otherwise the first non empty UserID will be * displayed. * * @note The resulting string will contains links to the key in the following format "key:<fingerprint>". */ KLEO_EXPORT QString prettySignature(const GpgME::Signature &signature, const QString &sender); // clang-format off enum ToolTipOption { KeyID = 0x001, Validity = 0x002, StorageLocation = 0x004, SerialNumber = 0x008, Issuer = 0x010, Subject = 0x020, ExpiryDates = 0x040, CertificateType = 0x080, CertificateUsage = 0x100, Fingerprint = 0x200, UserIDs = 0x400, OwnerTrust = 0x800, Subkeys = 0x1000, AllOptions = 0xffff }; // clang-format on KLEO_EXPORT QString toolTip(const GpgME::Key &key, int opts); KLEO_EXPORT QString toolTip(const Kleo::KeyGroup &group, int opts); KLEO_EXPORT QString toolTip(const GpgME::UserID &userID, int opts); /// Returns expiration date of @p key as string, or @p noExpiration if the key doesn't expire. KLEO_EXPORT QString expirationDateString(const GpgME::Key &key, const QString &noExpiration = {}); /// Returns expiration date of @p subkey as string, or @p noExpiration if the subkey doesn't expire. KLEO_EXPORT QString expirationDateString(const GpgME::Subkey &subkey, const QString &noExpiration = {}); /// Returns expiration date of @p sig as string, or @p noExpiration if the signature doesn't expire. KLEO_EXPORT QString expirationDateString(const GpgME::UserID::Signature &sig, const QString &noExpiration = {}); KLEO_EXPORT QDate expirationDate(const GpgME::Key &key); KLEO_EXPORT QDate expirationDate(const GpgME::Subkey &subkey); KLEO_EXPORT QDate expirationDate(const GpgME::UserID::Signature &sig); /** * Returns expiration date of @p key as string suitable for screen readers. * If the key doesn't expire, then it returns @p noExpiration if @p noExpiration is not empty. Otherwise, * returns the localization of "unlimited". */ KLEO_EXPORT QString accessibleExpirationDate(const GpgME::Key &key, const QString &noExpiration = {}); /** * Returns expiration date of @p subkey as string suitable for screen readers. * If the subkey doesn't expire, then it returns @p noExpiration if @p noExpiration is not empty. Otherwise, * returns the localization of "unlimited". */ KLEO_EXPORT QString accessibleExpirationDate(const GpgME::Subkey &subkey, const QString &noExpiration = {}); /** * Returns expiration date of @p sig as string suitable for screen readers. * If the signature doesn't expire, then it returns @p noExpiration if @p noExpiration is not empty. Otherwise, * returns the localization of "unlimited". */ KLEO_EXPORT QString accessibleExpirationDate(const GpgME::UserID::Signature &sig, const QString &noExpiration = {}); KLEO_EXPORT QString creationDateString(const GpgME::Key &key); KLEO_EXPORT QString creationDateString(const GpgME::Subkey &subkey); KLEO_EXPORT QString creationDateString(const GpgME::UserID::Signature &sig); KLEO_EXPORT QDate creationDate(const GpgME::Key &key); KLEO_EXPORT QDate creationDate(const GpgME::Subkey &subkey); KLEO_EXPORT QDate creationDate(const GpgME::UserID::Signature &sig); KLEO_EXPORT QString accessibleCreationDate(const GpgME::Key &key); KLEO_EXPORT QString accessibleCreationDate(const GpgME::Subkey &subkey); /* Convert a GPGME style time or a QDate to a localized string */ KLEO_EXPORT QString dateString(time_t t); KLEO_EXPORT QString dateString(const QDate &date); KLEO_EXPORT QString accessibleDate(time_t t); KLEO_EXPORT QString accessibleDate(const QDate &date); KLEO_EXPORT QString displayName(GpgME::Protocol prot); KLEO_EXPORT QString type(const GpgME::Key &key); KLEO_EXPORT QString type(const GpgME::Subkey &subkey); KLEO_EXPORT QString type(const Kleo::KeyGroup &group); KLEO_EXPORT QString ownerTrustShort(const GpgME::Key &key); KLEO_EXPORT QString ownerTrustShort(GpgME::Key::OwnerTrust trust); KLEO_EXPORT QString validityShort(const GpgME::Subkey &subkey); KLEO_EXPORT QString validityShort(const GpgME::UserID &uid); KLEO_EXPORT QString validityShort(const GpgME::UserID::Signature &sig); KLEO_EXPORT QIcon validityIcon(const GpgME::UserID::Signature &sig); /* A sentence about the validity of the UserID */ KLEO_EXPORT QString validity(const GpgME::UserID &uid); KLEO_EXPORT QString validity(const Kleo::KeyGroup &group); KLEO_EXPORT QIcon validityIcon(const Kleo::KeyGroup &group); KLEO_EXPORT QString formatForComboBox(const GpgME::Key &key); KLEO_EXPORT QString formatKeyLink(const GpgME::Key &key); KLEO_EXPORT QString signatureToString(const GpgME::Signature &sig, const GpgME::Key &key); KLEO_EXPORT const char *summaryToString(const GpgME::Signature::Summary summary); KLEO_EXPORT QString importMetaData(const GpgME::Import &import); KLEO_EXPORT QString importMetaData(const GpgME::Import &import, const QStringList &sources); KLEO_EXPORT QString formatOverview(const GpgME::Key &key); KLEO_EXPORT QString usageString(const GpgME::Subkey &subkey); KLEO_EXPORT QString summaryLine(const GpgME::UserID &id); KLEO_EXPORT QString summaryLine(const GpgME::Key &key); KLEO_EXPORT QString summaryLine(const KeyGroup &group); KLEO_EXPORT QString nameAndEmailForSummaryLine(const GpgME::Key &key); KLEO_EXPORT QString nameAndEmailForSummaryLine(const GpgME::UserID &id); KLEO_EXPORT QIcon iconForUid(const GpgME::UserID &uid); /* The compliance mode of the gnupg system. Empty if compliance * mode is not set. * Use Kleo::gnupgComplianceMode() instead. */ KLEO_DEPRECATED_EXPORT QString complianceMode(); /* A sentence if the key confirms to the current compliance mode */ KLEO_EXPORT QString complianceStringForKey(const GpgME::Key &key); KLEO_EXPORT QString complianceStringForUserID(const GpgME::UserID &userID); /* A single word for use in keylists to describe the validity of the * given key, including any conformance statements relevant to the * current conformance mode. */ KLEO_EXPORT QString complianceStringShort(const GpgME::Key &key); KLEO_EXPORT QString complianceStringShort(const GpgME::UserID &id); KLEO_EXPORT QString complianceStringShort(const Kleo::KeyGroup &group); /* The origin of the key mapped to a localized string */ KLEO_EXPORT QString origin(int o); /* Human-readable trust signature scope (for trust signature regexp created by GnuPG) */ KLEO_EXPORT QString trustSignatureDomain(const GpgME::UserID::Signature &sig); /* Summary of trust signature properties */ KLEO_EXPORT QString trustSignature(const GpgME::UserID::Signature &sig); /** * Returns the value of Error::asString() for the error \p error as Unicode string. */ KLEO_EXPORT QString errorAsString(const GpgME::Error &error); /** * Returns a name suitable for being displayed for the GPG algorithm name @p algorithm. */ KLEO_EXPORT QString prettyAlgorithmName(const std::string &algorithm); /** * Returns the email associated to a UserID. */ KLEO_EXPORT QString email(const GpgME::UserID &uid); } }