File indexing completed on 2025-01-05 04:55:52

0001 /* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*-
0002     utils/formatting.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
0006     SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH
0007     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include <config-libkleo.h>
0013 
0014 #include "formatting.h"
0015 
0016 #include "algorithm.h"
0017 #include "compat.h"
0018 #include "compliance.h"
0019 #include "cryptoconfig.h"
0020 #include "gnupg.h"
0021 #include "keyhelpers.h"
0022 
0023 #include <libkleo/dn.h>
0024 #include <libkleo/keycache.h>
0025 #include <libkleo/keygroup.h>
0026 
0027 #include <libkleo_debug.h>
0028 
0029 #include <KEmailAddress>
0030 #include <KLocalizedString>
0031 
0032 #include <QGpgME/CryptoConfig>
0033 #include <QGpgME/Protocol>
0034 
0035 #include <QDateTime>
0036 #include <QIcon>
0037 #include <QLocale>
0038 #include <QRegularExpression>
0039 #include <QString>
0040 #include <QTextDocument> // for Qt::escape
0041 
0042 #include <gpgme++/importresult.h>
0043 #include <gpgme++/key.h>
0044 
0045 #include <gpg-error.h>
0046 
0047 using namespace GpgME;
0048 using namespace Kleo;
0049 
0050 namespace
0051 {
0052 QIcon iconForValidityAndCompliance(UserID::Validity validity, bool isCompliant)
0053 {
0054     switch (validity) {
0055     case UserID::Ultimate:
0056     case UserID::Full:
0057     case UserID::Marginal:
0058         return isCompliant ? Formatting::successIcon() : Formatting::infoIcon();
0059     case UserID::Never:
0060         return Formatting::errorIcon();
0061     case UserID::Undefined:
0062     case UserID::Unknown:
0063     default:
0064         return Formatting::infoIcon();
0065     }
0066 }
0067 QIcon iconForValidity(const UserID &userId)
0068 {
0069     const bool keyIsCompliant = !DeVSCompliance::isActive() || //
0070         (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(userId.parent()));
0071     return iconForValidityAndCompliance(userId.validity(), keyIsCompliant);
0072 }
0073 }
0074 
0075 QIcon Formatting::IconProvider::icon(const GpgME::Key &key) const
0076 {
0077     return icon(key.userID(0));
0078 }
0079 
0080 QIcon Formatting::IconProvider::icon(const GpgME::UserID &userID) const
0081 {
0082     if (usage.canEncrypt() && !Kleo::canBeUsedForEncryption(userID.parent())) {
0083         return Formatting::errorIcon();
0084     }
0085     if (usage.canSign() && !Kleo::canBeUsedForSigning(userID.parent())) {
0086         return Formatting::errorIcon();
0087     }
0088     if (userID.parent().isBad() || userID.isBad()) {
0089         return Formatting::errorIcon();
0090     }
0091     if (Kleo::isRevokedOrExpired(userID)) {
0092         return Formatting::errorIcon();
0093     }
0094     return iconForValidity(userID);
0095 }
0096 
0097 QIcon Formatting::IconProvider::icon(const KeyGroup &group) const
0098 {
0099     if (usage.canEncrypt() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForEncryption)) {
0100         return Formatting::errorIcon();
0101     }
0102     if (usage.canSign() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForSigning)) {
0103         return Formatting::errorIcon();
0104     }
0105     return validityIcon(group);
0106 }
0107 
0108 QIcon Formatting::successIcon()
0109 {
0110     return QIcon::fromTheme(QStringLiteral("emblem-success"));
0111 }
0112 
0113 QIcon Formatting::infoIcon()
0114 {
0115     return QIcon::fromTheme(QStringLiteral("emblem-information"));
0116 }
0117 
0118 QIcon Formatting::questionIcon()
0119 {
0120     return QIcon::fromTheme(QStringLiteral("emblem-question"));
0121 }
0122 
0123 QIcon Formatting::unavailableIcon()
0124 {
0125     return QIcon::fromTheme(QStringLiteral("emblem-unavailable"));
0126 }
0127 
0128 QIcon Formatting::warningIcon()
0129 {
0130     return QIcon::fromTheme(QStringLiteral("emblem-warning"));
0131 }
0132 
0133 QIcon Formatting::errorIcon()
0134 {
0135     return QIcon::fromTheme(QStringLiteral("emblem-error"));
0136 }
0137 
0138 //
0139 // Name
0140 //
0141 
0142 QString Formatting::prettyName(int proto, const char *id, const char *name_, const char *comment_)
0143 {
0144     if (proto == GpgME::OpenPGP) {
0145         const QString name = QString::fromUtf8(name_);
0146         if (name.isEmpty()) {
0147             return QString();
0148         }
0149         const QString comment = QString::fromUtf8(comment_);
0150         if (comment.isEmpty()) {
0151             return name;
0152         }
0153         return QStringLiteral("%1 (%2)").arg(name, comment);
0154     }
0155 
0156     if (proto == GpgME::CMS) {
0157         const DN subject(id);
0158         const QString cn = subject[QStringLiteral("CN")].trimmed();
0159         if (cn.isEmpty()) {
0160             return subject.prettyDN();
0161         }
0162         return cn;
0163     }
0164 
0165     return QString();
0166 }
0167 
0168 QString Formatting::prettyNameAndEMail(int proto, const char *id, const char *name_, const char *email_, const char *comment_)
0169 {
0170     return prettyNameAndEMail(proto, QString::fromUtf8(id), QString::fromUtf8(name_), prettyEMail(email_, id), QString::fromUtf8(comment_));
0171 }
0172 
0173 QString Formatting::prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment)
0174 {
0175     if (proto == GpgME::OpenPGP) {
0176         if (name.isEmpty()) {
0177             if (email.isEmpty()) {
0178                 return QString();
0179             } else if (comment.isEmpty()) {
0180                 return QStringLiteral("<%1>").arg(email);
0181             } else {
0182                 return QStringLiteral("(%2) <%1>").arg(email, comment);
0183             }
0184         }
0185         if (email.isEmpty()) {
0186             if (comment.isEmpty()) {
0187                 return name;
0188             } else {
0189                 return QStringLiteral("%1 (%2)").arg(name, comment);
0190             }
0191         }
0192         if (comment.isEmpty()) {
0193             return QStringLiteral("%1 <%2>").arg(name, email);
0194         } else {
0195             return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment);
0196         }
0197     }
0198 
0199     if (proto == GpgME::CMS) {
0200         const DN subject(id);
0201         const QString cn = subject[QStringLiteral("CN")].trimmed();
0202         if (cn.isEmpty()) {
0203             return subject.prettyDN();
0204         }
0205         return cn;
0206     }
0207     return QString();
0208 }
0209 
0210 QString Formatting::prettyUserID(const UserID &uid)
0211 {
0212     if (uid.parent().protocol() == GpgME::OpenPGP) {
0213         return prettyNameAndEMail(uid);
0214     }
0215     const QByteArray id = QByteArray(uid.id()).trimmed();
0216     if (id.startsWith('<')) {
0217         return prettyEMail(uid.email(), uid.id());
0218     }
0219     if (id.startsWith('(')) {
0220         // ### parse uri/dns:
0221         return QString::fromUtf8(uid.id());
0222     } else {
0223         return DN(uid.id()).prettyDN();
0224     }
0225 }
0226 
0227 QString Formatting::prettyKeyID(const char *id)
0228 {
0229     if (!id) {
0230         return QString();
0231     }
0232     return QLatin1StringView("0x") + QString::fromLatin1(id).toUpper();
0233 }
0234 
0235 QString Formatting::prettyNameAndEMail(const UserID &uid)
0236 {
0237     return prettyNameAndEMail(uid.parent().protocol(), uid.id(), uid.name(), uid.email(), uid.comment());
0238 }
0239 
0240 QString Formatting::prettyNameAndEMail(const Key &key)
0241 {
0242     return prettyNameAndEMail(key.userID(0));
0243 }
0244 
0245 QString Formatting::prettyName(const Key &key)
0246 {
0247     return prettyName(key.userID(0));
0248 }
0249 
0250 QString Formatting::prettyName(const UserID &uid)
0251 {
0252     return prettyName(uid.parent().protocol(), uid.id(), uid.name(), uid.comment());
0253 }
0254 
0255 QString Formatting::prettyName(const UserID::Signature &sig)
0256 {
0257     return prettyName(GpgME::OpenPGP, sig.signerUserID(), sig.signerName(), sig.signerComment());
0258 }
0259 
0260 //
0261 // EMail
0262 //
0263 
0264 QString Formatting::prettyEMail(const Key &key)
0265 {
0266     for (unsigned int i = 0, end = key.numUserIDs(); i < end; ++i) {
0267         const QString email = prettyEMail(key.userID(i));
0268         if (!email.isEmpty()) {
0269             return email;
0270         }
0271     }
0272     return QString();
0273 }
0274 
0275 QString Formatting::prettyEMail(const UserID &uid)
0276 {
0277     return prettyEMail(uid.email(), uid.id());
0278 }
0279 
0280 QString Formatting::prettyEMail(const UserID::Signature &sig)
0281 {
0282     return prettyEMail(sig.signerEmail(), sig.signerUserID());
0283 }
0284 
0285 QString Formatting::prettyEMail(const char *email_, const char *id)
0286 {
0287     QString email;
0288     QString name;
0289     QString comment;
0290     if (email_ && KEmailAddress::splitAddress(QString::fromUtf8(email_), name, email, comment) == KEmailAddress::AddressOk) {
0291         return email;
0292     } else {
0293         return DN(id)[QStringLiteral("EMAIL")].trimmed();
0294     }
0295 }
0296 
0297 //
0298 // Tooltip
0299 //
0300 
0301 namespace
0302 {
0303 
0304 static QString protect_whitespace(QString s)
0305 {
0306     static const QLatin1Char SP(' ');
0307     static const QLatin1Char NBSP('\xA0');
0308     return s.replace(SP, NBSP);
0309 }
0310 
0311 template<typename T_arg>
0312 QString format_row(const QString &field, const T_arg &arg)
0313 {
0314     return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").arg(protect_whitespace(field), arg);
0315 }
0316 QString format_row(const QString &field, const QString &arg)
0317 {
0318     return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").arg(protect_whitespace(field), arg.toHtmlEscaped());
0319 }
0320 QString format_row(const QString &field, const char *arg)
0321 {
0322     return format_row(field, QString::fromUtf8(arg));
0323 }
0324 
0325 QString format_keytype(const Key &key)
0326 {
0327     const Subkey subkey = key.subkey(0);
0328     if (key.hasSecret()) {
0329         return i18n("%1-bit %2 (secret key available)", subkey.length(), QLatin1StringView(subkey.publicKeyAlgorithmAsString()));
0330     } else {
0331         return i18n("%1-bit %2", subkey.length(), QLatin1StringView(subkey.publicKeyAlgorithmAsString()));
0332     }
0333 }
0334 
0335 QString format_subkeytype(const Subkey &subkey)
0336 {
0337     const auto algo = subkey.publicKeyAlgorithm();
0338 
0339     if (algo == Subkey::AlgoECC || algo == Subkey::AlgoECDSA || algo == Subkey::AlgoECDH || algo == Subkey::AlgoEDDSA) {
0340         return QString::fromStdString(subkey.algoName());
0341     }
0342     return i18n("%1-bit %2", subkey.length(), QLatin1StringView(subkey.publicKeyAlgorithmAsString()));
0343 }
0344 
0345 QString format_keyusage(const Key &key)
0346 {
0347     QStringList capabilities;
0348     if (Kleo::keyHasSign(key)) {
0349         if (key.isQualified()) {
0350             capabilities.push_back(i18n("Signing (Qualified)"));
0351         } else {
0352             capabilities.push_back(i18n("Signing"));
0353         }
0354     }
0355     if (Kleo::keyHasEncrypt(key)) {
0356         capabilities.push_back(i18n("Encryption"));
0357     }
0358     if (Kleo::keyHasCertify(key)) {
0359         capabilities.push_back(i18n("Certifying User-IDs"));
0360     }
0361     if (Kleo::keyHasAuthenticate(key)) {
0362         capabilities.push_back(i18n("SSH Authentication"));
0363     }
0364     return capabilities.join(QLatin1StringView(", "));
0365 }
0366 
0367 QString format_subkeyusage(const Subkey &subkey)
0368 {
0369     QStringList capabilities;
0370     if (subkey.canSign()) {
0371         if (subkey.isQualified()) {
0372             capabilities.push_back(i18n("Signing (Qualified)"));
0373         } else {
0374             capabilities.push_back(i18n("Signing"));
0375         }
0376     }
0377     if (subkey.canEncrypt()) {
0378         capabilities.push_back(i18n("Encryption"));
0379     }
0380     if (subkey.canCertify()) {
0381         capabilities.push_back(i18n("Certifying User-IDs"));
0382     }
0383     if (subkey.canAuthenticate()) {
0384         capabilities.push_back(i18n("SSH Authentication"));
0385     }
0386     return capabilities.join(QLatin1StringView(", "));
0387 }
0388 
0389 static QString time_t2string(time_t t)
0390 {
0391     const QDateTime dt = QDateTime::fromSecsSinceEpoch(quint32(t));
0392     return QLocale().toString(dt, QLocale::ShortFormat);
0393 }
0394 
0395 static QString make_red(const QString &txt)
0396 {
0397     return QLatin1StringView("<font color=\"red\">") + txt.toHtmlEscaped() + QLatin1String("</font>");
0398 }
0399 
0400 }
0401 
0402 static QString toolTipInternal(const GpgME::Key &key, const GpgME::UserID &userID, int flags)
0403 {
0404     if (flags == 0 || (key.protocol() != GpgME::CMS && key.protocol() != GpgME::OpenPGP)) {
0405         return QString();
0406     }
0407 
0408     const Subkey subkey = key.subkey(0);
0409 
0410     QString result;
0411     if (flags & Formatting::Validity) {
0412         if (key.protocol() == GpgME::OpenPGP || (key.keyListMode() & Validate)) {
0413             if (userID.isRevoked() || key.isRevoked()) {
0414                 result = make_red(i18n("Revoked"));
0415             } else if (key.isExpired()) {
0416                 result = make_red(i18n("Expired"));
0417             } else if (key.isDisabled()) {
0418                 result = i18n("Disabled");
0419             } else if (key.keyListMode() & GpgME::Validate) {
0420                 if (!userID.isNull()) {
0421                     if (userID.validity() >= UserID::Validity::Full) {
0422                         result = i18n("User-ID is certified.");
0423                         const auto compliance = Formatting::complianceStringForUserID(userID);
0424                         if (!compliance.isEmpty()) {
0425                             result += QStringLiteral("<br>") + compliance;
0426                         }
0427                     } else {
0428                         result = i18n("User-ID is not certified.");
0429                     }
0430                 } else {
0431                     unsigned int fullyTrusted = 0;
0432                     for (const auto &uid : key.userIDs()) {
0433                         if (uid.validity() >= UserID::Validity::Full) {
0434                             fullyTrusted++;
0435                         }
0436                     }
0437                     if (fullyTrusted == key.numUserIDs()) {
0438                         result = i18n("All User-IDs are certified.");
0439                         const auto compliance = Formatting::complianceStringForKey(key);
0440                         if (!compliance.isEmpty()) {
0441                             result += QStringLiteral("<br>") + compliance;
0442                         }
0443                     } else {
0444                         result = i18np("One User-ID is not certified.", "%1 User-IDs are not certified.", key.numUserIDs() - fullyTrusted);
0445                     }
0446                 }
0447             } else {
0448                 result = i18n("The validity cannot be checked at the moment.");
0449             }
0450         } else {
0451             result = i18n("The validity cannot be checked at the moment.");
0452         }
0453     }
0454     if (flags == Formatting::Validity) {
0455         return result;
0456     }
0457 
0458     result += QLatin1StringView("<table border=\"0\">");
0459     if (key.protocol() == GpgME::CMS) {
0460         if (flags & Formatting::SerialNumber) {
0461             result += format_row(i18n("Serial number"), key.issuerSerial());
0462         }
0463         if (flags & Formatting::Issuer) {
0464             result += format_row(i18n("Issuer"), key.issuerName());
0465         }
0466     }
0467     if (flags & Formatting::UserIDs) {
0468         if (userID.isNull()) {
0469             const std::vector<UserID> uids = key.userIDs();
0470             if (!uids.empty()) {
0471                 result += format_row(key.protocol() == GpgME::CMS ? i18n("Subject") : i18n("User-ID"), Formatting::prettyUserID(uids.front()));
0472             }
0473             if (uids.size() > 1) {
0474                 for (auto it = uids.begin() + 1, end = uids.end(); it != end; ++it) {
0475                     if (!it->isRevoked() && !it->isInvalid()) {
0476                         result += format_row(i18n("a.k.a."), Formatting::prettyUserID(*it));
0477                     }
0478                 }
0479             }
0480         } else {
0481             result += format_row(key.protocol() == GpgME::CMS ? i18n("Subject") : i18n("User-ID"), Formatting::prettyUserID(userID));
0482         }
0483     }
0484     if (flags & Formatting::ExpiryDates) {
0485         result += format_row(i18n("Valid from"), time_t2string(subkey.creationTime()));
0486 
0487         if (!subkey.neverExpires()) {
0488             result += format_row(i18n("Valid until"), time_t2string(subkey.expirationTime()));
0489         }
0490     }
0491 
0492     if (flags & Formatting::CertificateType) {
0493         result += format_row(i18n("Type"), format_keytype(key));
0494     }
0495     if (flags & Formatting::CertificateUsage) {
0496         result += format_row(i18n("Usage"), format_keyusage(key));
0497     }
0498     if (flags & Formatting::KeyID) {
0499         result += format_row(i18n("Key-ID"), QString::fromLatin1(key.shortKeyID()));
0500     }
0501     if (flags & Formatting::Fingerprint) {
0502         result += format_row(i18n("Fingerprint"), key.primaryFingerprint());
0503     }
0504     if (flags & Formatting::OwnerTrust) {
0505         if (key.protocol() == GpgME::OpenPGP) {
0506             result += format_row(i18n("Certification trust"), Formatting::ownerTrustShort(key));
0507         } else if (key.isRoot()) {
0508             result += format_row(i18n("Trusted issuer?"), (userID.isNull() ? key.userID(0) : userID).validity() == UserID::Ultimate ? i18n("Yes") : i18n("No"));
0509         }
0510     }
0511     if (flags & Formatting::StorageLocation) {
0512         if (const char *card = subkey.cardSerialNumber()) {
0513             result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card)));
0514         } else {
0515             result += format_row(i18n("Stored"), i18nc("stored...", "on this computer"));
0516         }
0517     }
0518     if (flags & Formatting::Subkeys) {
0519         for (const auto &sub : key.subkeys()) {
0520             result += QLatin1StringView("<hr/>");
0521             result += format_row(i18n("Subkey"), sub.fingerprint());
0522             if (sub.isRevoked()) {
0523                 result += format_row(i18n("Status"), i18n("Revoked"));
0524             } else if (sub.isExpired()) {
0525                 result += format_row(i18n("Status"), i18n("Expired"));
0526             }
0527             if (flags & Formatting::ExpiryDates) {
0528                 result += format_row(i18n("Valid from"), time_t2string(sub.creationTime()));
0529 
0530                 if (!sub.neverExpires()) {
0531                     result += format_row(i18n("Valid until"), time_t2string(sub.expirationTime()));
0532                 }
0533             }
0534 
0535             if (flags & Formatting::CertificateType) {
0536                 result += format_row(i18n("Type"), format_subkeytype(sub));
0537             }
0538             if (flags & Formatting::CertificateUsage) {
0539                 result += format_row(i18n("Usage"), format_subkeyusage(sub));
0540             }
0541             if (flags & Formatting::StorageLocation) {
0542                 if (const char *card = sub.cardSerialNumber()) {
0543                     result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card)));
0544                 } else {
0545                     result += format_row(i18n("Stored"), i18nc("stored...", "on this computer"));
0546                 }
0547             }
0548         }
0549     }
0550     result += QLatin1StringView("</table>");
0551 
0552     return result;
0553 }
0554 
0555 QString Formatting::toolTip(const Key &key, int flags)
0556 {
0557     return toolTipInternal(key, UserID(), flags);
0558 }
0559 
0560 namespace
0561 {
0562 template<typename Container>
0563 QString getValidityStatement(const Container &keys)
0564 {
0565     const bool allKeysAreOpenPGP = std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) {
0566         return key.protocol() == GpgME::OpenPGP;
0567     });
0568     const bool allKeysAreValidated = std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) {
0569         return key.keyListMode() & Validate;
0570     });
0571     if (allKeysAreOpenPGP || allKeysAreValidated) {
0572         const bool someKeysAreBad = std::any_of(keys.cbegin(), keys.cend(), std::mem_fn(&Key::isBad));
0573         if (someKeysAreBad) {
0574             return i18n("Some keys are revoked, expired, disabled, or invalid.");
0575         } else {
0576             const bool allKeysAreFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Kleo::allUserIDsHaveFullValidity);
0577             if (allKeysAreFullyValid) {
0578                 return i18n("All keys are certified.");
0579             } else {
0580                 return i18n("Some keys are not certified.");
0581             }
0582         }
0583     }
0584     return i18n("The validity of the keys cannot be checked at the moment.");
0585 }
0586 }
0587 
0588 QString Formatting::toolTip(const KeyGroup &group, int flags)
0589 {
0590     static const unsigned int maxNumKeysForTooltip = 20;
0591 
0592     if (group.isNull()) {
0593         return QString();
0594     }
0595 
0596     const KeyGroup::Keys &keys = group.keys();
0597     if (keys.size() == 0) {
0598         return i18nc("@info:tooltip", "This group does not contain any keys.");
0599     }
0600 
0601     const QString validity = (flags & Validity) ? getValidityStatement(keys) : QString();
0602     if (flags == Validity) {
0603         return validity;
0604     }
0605 
0606     // list either up to maxNumKeysForTooltip keys or (maxNumKeysForTooltip-1) keys followed by "and n more keys"
0607     const unsigned int numKeysForTooltip = keys.size() > maxNumKeysForTooltip ? maxNumKeysForTooltip - 1 : keys.size();
0608 
0609     QStringList result;
0610     result.reserve(3 + 2 + numKeysForTooltip + 2);
0611     if (!validity.isEmpty()) {
0612         result.push_back(QStringLiteral("<p>"));
0613         result.push_back(validity.toHtmlEscaped());
0614         result.push_back(QStringLiteral("</p>"));
0615     }
0616 
0617     result.push_back(QStringLiteral("<p>"));
0618     result.push_back(i18n("Keys:"));
0619     {
0620         auto it = keys.cbegin();
0621         for (unsigned int i = 0; i < numKeysForTooltip; ++i, ++it) {
0622             result.push_back(QLatin1StringView("<br>") + Formatting::summaryLine(*it).toHtmlEscaped());
0623         }
0624     }
0625     if (keys.size() > numKeysForTooltip) {
0626         result.push_back(QLatin1StringView("<br>")
0627                          + i18ncp("this follows a list of keys", "and 1 more key", "and %1 more keys", keys.size() - numKeysForTooltip));
0628     }
0629     result.push_back(QStringLiteral("</p>"));
0630 
0631     return result.join(QLatin1Char('\n'));
0632 }
0633 
0634 QString Formatting::toolTip(const UserID &userID, int flags)
0635 {
0636     return toolTipInternal(userID.parent(), userID, flags);
0637 }
0638 
0639 //
0640 // Creation and Expiration
0641 //
0642 
0643 namespace
0644 {
0645 static QDate time_t2date(time_t t)
0646 {
0647     if (!t) {
0648         return {};
0649     }
0650     const QDateTime dt = QDateTime::fromSecsSinceEpoch(quint32(t));
0651     return dt.date();
0652 }
0653 static QString accessible_date_format()
0654 {
0655     return i18nc(
0656         "date format suitable for screen readers; "
0657         "d: day as a number without a leading zero, "
0658         "MMMM: localized month name, "
0659         "yyyy: year as a four digit number",
0660         "MMMM d, yyyy");
0661 }
0662 
0663 template<typename T>
0664 QString expiration_date_string(const T &tee, const QString &noExpiration)
0665 {
0666     return tee.neverExpires() ? noExpiration : Formatting::dateString(time_t2date(tee.expirationTime()));
0667 }
0668 template<typename T>
0669 QDate creation_date(const T &tee)
0670 {
0671     return time_t2date(tee.creationTime());
0672 }
0673 template<typename T>
0674 QDate expiration_date(const T &tee)
0675 {
0676     return time_t2date(tee.expirationTime());
0677 }
0678 }
0679 
0680 QString Formatting::dateString(time_t t)
0681 {
0682     return dateString(time_t2date(t));
0683 }
0684 
0685 QString Formatting::dateString(const QDate &date)
0686 {
0687     return QLocale().toString(date, QLocale::ShortFormat);
0688 }
0689 
0690 QString Formatting::accessibleDate(time_t t)
0691 {
0692     return accessibleDate(time_t2date(t));
0693 }
0694 
0695 QString Formatting::accessibleDate(const QDate &date)
0696 {
0697     return QLocale().toString(date, accessible_date_format());
0698 }
0699 
0700 QString Formatting::expirationDateString(const Key &key, const QString &noExpiration)
0701 {
0702     // if key is remote but has a non-zero expiration date (e.g. a key looked up via WKD),
0703     // then we assume that the date is valid; if the date is zero for a remote key, then
0704     // we don't know if it's unknown or unlimited
0705     return isRemoteKey(key) && (key.subkey(0).expirationTime() == 0) //
0706         ? i18nc("@info the expiration date of the key is unknown", "unknown")
0707         : expiration_date_string(key.subkey(0), noExpiration);
0708 }
0709 
0710 QString Formatting::expirationDateString(const Subkey &subkey, const QString &noExpiration)
0711 {
0712     return expiration_date_string(subkey, noExpiration);
0713 }
0714 
0715 QString Formatting::expirationDateString(const UserID::Signature &sig, const QString &noExpiration)
0716 {
0717     return expiration_date_string(sig, noExpiration);
0718 }
0719 
0720 QDate Formatting::expirationDate(const Key &key)
0721 {
0722     return expiration_date(key.subkey(0));
0723 }
0724 
0725 QDate Formatting::expirationDate(const Subkey &subkey)
0726 {
0727     return expiration_date(subkey);
0728 }
0729 
0730 QDate Formatting::expirationDate(const UserID::Signature &sig)
0731 {
0732     return expiration_date(sig);
0733 }
0734 
0735 QString Formatting::accessibleExpirationDate(const Key &key, const QString &noExpiration)
0736 {
0737     // if key is remote but has a non-zero expiration date (e.g. a key looked up via WKD),
0738     // then we assume that the date is valid; if the date is zero for a remote key, then
0739     // we don't know if it's unknown or unlimited
0740     return isRemoteKey(key) && (key.subkey(0).expirationTime() == 0) //
0741         ? i18nc("@info the expiration date of the key is unknown", "unknown")
0742         : accessibleExpirationDate(key.subkey(0), noExpiration);
0743 }
0744 
0745 QString Formatting::accessibleExpirationDate(const Subkey &subkey, const QString &noExpiration)
0746 {
0747     if (subkey.neverExpires()) {
0748         return noExpiration.isEmpty() ? i18n("unlimited") : noExpiration;
0749     } else {
0750         return accessibleDate(expirationDate(subkey));
0751     }
0752 }
0753 
0754 QString Formatting::accessibleExpirationDate(const UserID::Signature &sig, const QString &noExpiration)
0755 {
0756     if (sig.neverExpires()) {
0757         return noExpiration.isEmpty() ? i18n("unlimited") : noExpiration;
0758     } else {
0759         return accessibleDate(expirationDate(sig));
0760     }
0761 }
0762 
0763 QString Formatting::creationDateString(const Key &key)
0764 {
0765     return dateString(creation_date(key.subkey(0)));
0766 }
0767 
0768 QString Formatting::creationDateString(const Subkey &subkey)
0769 {
0770     return dateString(creation_date(subkey));
0771 }
0772 
0773 QString Formatting::creationDateString(const UserID::Signature &sig)
0774 {
0775     return dateString(creation_date(sig));
0776 }
0777 
0778 QDate Formatting::creationDate(const Key &key)
0779 {
0780     return creation_date(key.subkey(0));
0781 }
0782 
0783 QDate Formatting::creationDate(const Subkey &subkey)
0784 {
0785     return creation_date(subkey);
0786 }
0787 
0788 QDate Formatting::creationDate(const UserID::Signature &sig)
0789 {
0790     return creation_date(sig);
0791 }
0792 
0793 QString Formatting::accessibleCreationDate(const Key &key)
0794 {
0795     return accessibleDate(creationDate(key));
0796 }
0797 
0798 QString Formatting::accessibleCreationDate(const Subkey &subkey)
0799 {
0800     return accessibleDate(creationDate(subkey));
0801 }
0802 
0803 //
0804 // Types
0805 //
0806 
0807 QString Formatting::displayName(GpgME::Protocol p)
0808 {
0809     if (p == GpgME::CMS) {
0810         return i18nc("X.509/CMS encryption standard", "S/MIME");
0811     }
0812     if (p == GpgME::OpenPGP) {
0813         return i18n("OpenPGP");
0814     }
0815     return i18nc("Unknown encryption protocol", "Unknown");
0816 }
0817 
0818 QString Formatting::type(const Key &key)
0819 {
0820     return displayName(key.protocol());
0821 }
0822 
0823 QString Formatting::type(const Subkey &subkey)
0824 {
0825     return QString::fromUtf8(subkey.publicKeyAlgorithmAsString());
0826 }
0827 
0828 QString Formatting::type(const KeyGroup &group)
0829 {
0830     Q_UNUSED(group)
0831     return i18nc("a group of keys/certificates", "Group");
0832 }
0833 
0834 //
0835 // Status / Validity
0836 //
0837 
0838 QString Formatting::ownerTrustShort(const Key &key)
0839 {
0840     return ownerTrustShort(key.ownerTrust());
0841 }
0842 
0843 QString Formatting::ownerTrustShort(Key::OwnerTrust trust)
0844 {
0845     switch (trust) {
0846     case Key::Unknown:
0847         return i18nc("unknown trust level", "unknown");
0848     case Key::Never:
0849         return i18n("untrusted");
0850     case Key::Marginal:
0851         return i18nc("marginal trust", "marginal");
0852     case Key::Full:
0853         return i18nc("full trust", "full");
0854     case Key::Ultimate:
0855         return i18nc("ultimate trust", "ultimate");
0856     case Key::Undefined:
0857         return i18nc("undefined trust", "undefined");
0858     default:
0859         Q_ASSERT(!"unexpected owner trust value");
0860         break;
0861     }
0862     return QString();
0863 }
0864 
0865 QString Formatting::validityShort(const Subkey &subkey)
0866 {
0867     if (subkey.isRevoked()) {
0868         return i18n("revoked");
0869     }
0870     if (subkey.isExpired()) {
0871         return i18n("expired");
0872     }
0873     if (subkey.isDisabled()) {
0874         return i18n("disabled");
0875     }
0876     if (subkey.isInvalid()) {
0877         return i18n("invalid");
0878     }
0879     return i18nc("as in good/valid signature", "good");
0880 }
0881 
0882 QString Formatting::validityShort(const UserID &uid)
0883 {
0884     if (uid.isRevoked()) {
0885         return i18n("revoked");
0886     }
0887     if (uid.isInvalid()) {
0888         return i18n("invalid");
0889     }
0890     switch (uid.validity()) {
0891     case UserID::Unknown:
0892         return i18nc("unknown trust level", "unknown");
0893     case UserID::Undefined:
0894         return i18nc("undefined trust", "undefined");
0895     case UserID::Never:
0896         return i18n("untrusted");
0897     case UserID::Marginal:
0898         return i18nc("marginal trust", "marginal");
0899     case UserID::Full:
0900         return i18nc("full trust", "full");
0901     case UserID::Ultimate:
0902         return i18nc("ultimate trust", "ultimate");
0903     }
0904     return QString();
0905 }
0906 
0907 QString Formatting::validityShort(const UserID::Signature &sig)
0908 {
0909     switch (sig.status()) {
0910     case UserID::Signature::NoError:
0911         if (!sig.isInvalid()) {
0912             /* See RFC 4880 Section 5.2.1 */
0913             switch (sig.certClass()) {
0914             case 0x10: /* Generic */
0915             case 0x11: /* Persona */
0916             case 0x12: /* Casual */
0917             case 0x13: /* Positive */
0918                 return i18n("valid");
0919             case 0x30:
0920                 return i18n("revoked");
0921             default:
0922                 return i18n("class %1", sig.certClass());
0923             }
0924         }
0925         [[fallthrough]];
0926         // fall through:
0927     case UserID::Signature::GeneralError:
0928         return i18n("invalid");
0929     case UserID::Signature::SigExpired:
0930         return i18n("expired");
0931     case UserID::Signature::KeyExpired:
0932         return i18n("certificate expired");
0933     case UserID::Signature::BadSignature:
0934         return i18nc("fake/invalid signature", "bad");
0935     case UserID::Signature::NoPublicKey: {
0936         /* GnuPG returns the same error for no public key as for expired
0937          * or revoked certificates. */
0938         const auto key = KeyCache::instance()->findByKeyIDOrFingerprint(sig.signerKeyID());
0939         if (key.isNull()) {
0940             return i18n("no public key");
0941         } else if (key.isExpired()) {
0942             return i18n("key expired");
0943         } else if (key.isRevoked()) {
0944             return i18n("key revoked");
0945         } else if (key.isDisabled()) {
0946             return i18n("key disabled");
0947         }
0948         /* can't happen */
0949         return QStringLiteral("unknown");
0950     }
0951     }
0952     return QString();
0953 }
0954 
0955 QIcon Formatting::validityIcon(const UserID::Signature &sig)
0956 {
0957     switch (sig.status()) {
0958     case UserID::Signature::NoError:
0959         if (!sig.isInvalid()) {
0960             /* See RFC 4880 Section 5.2.1 */
0961             switch (sig.certClass()) {
0962             case 0x10: /* Generic */
0963             case 0x11: /* Persona */
0964             case 0x12: /* Casual */
0965             case 0x13: /* Positive */
0966                 return Formatting::successIcon();
0967             case 0x30:
0968                 return Formatting::errorIcon();
0969             default:
0970                 return QIcon();
0971             }
0972         }
0973         [[fallthrough]];
0974         // fall through:
0975     case UserID::Signature::BadSignature:
0976     case UserID::Signature::GeneralError:
0977         return Formatting::errorIcon();
0978     case UserID::Signature::SigExpired:
0979     case UserID::Signature::KeyExpired:
0980         return Formatting::infoIcon();
0981     case UserID::Signature::NoPublicKey:
0982         return Formatting::questionIcon();
0983     }
0984     return QIcon();
0985 }
0986 
0987 QString Formatting::formatKeyLink(const Key &key)
0988 {
0989     if (key.isNull()) {
0990         return QString();
0991     }
0992     return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(QLatin1StringView(key.primaryFingerprint()), Formatting::prettyName(key));
0993 }
0994 
0995 QString Formatting::formatForComboBox(const GpgME::Key &key)
0996 {
0997     const QString name = prettyName(key);
0998     QString mail = prettyEMail(key);
0999     if (!mail.isEmpty()) {
1000         mail = QLatin1Char('<') + mail + QLatin1Char('>');
1001     }
1002     return i18nc("name, email, key id", "%1 %2 (%3)", name, mail, QLatin1StringView(key.shortKeyID())).simplified();
1003 }
1004 
1005 QString Formatting::nameAndEmailForSummaryLine(const UserID &id)
1006 {
1007     Q_ASSERT(!id.isNull());
1008 
1009     const QString email = Formatting::prettyEMail(id);
1010     const QString name = Formatting::prettyName(id);
1011 
1012     if (name.isEmpty()) {
1013         return email;
1014     } else if (email.isEmpty()) {
1015         return name;
1016     } else {
1017         return QStringLiteral("%1 <%2>").arg(name, email);
1018     }
1019 }
1020 
1021 QString Formatting::nameAndEmailForSummaryLine(const Key &key)
1022 {
1023     Q_ASSERT(!key.isNull());
1024 
1025     const QString email = Formatting::prettyEMail(key);
1026     const QString name = Formatting::prettyName(key);
1027 
1028     if (name.isEmpty()) {
1029         return email;
1030     } else if (email.isEmpty()) {
1031         return name;
1032     } else {
1033         return QStringLiteral("%1 <%2>").arg(name, email);
1034     }
1035 }
1036 
1037 const char *Formatting::summaryToString(const Signature::Summary summary)
1038 {
1039     if (summary & Signature::Red) {
1040         return "RED";
1041     }
1042     if (summary & Signature::Green) {
1043         return "GREEN";
1044     }
1045     return "YELLOW";
1046 }
1047 
1048 QString Formatting::signatureToString(const Signature &sig, const Key &key)
1049 {
1050     if (sig.isNull()) {
1051         return QString();
1052     }
1053 
1054     const bool red = (sig.summary() & Signature::Red);
1055     const bool valid = (sig.summary() & Signature::Valid);
1056 
1057     if (red) {
1058         if (key.isNull()) {
1059             if (const char *fpr = sig.fingerprint()) {
1060                 return i18n("Bad signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status()));
1061             } else {
1062                 return i18n("Bad signature by an unknown certificate: %1", Formatting::errorAsString(sig.status()));
1063             }
1064         } else {
1065             return i18n("Bad signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status()));
1066         }
1067 
1068     } else if (valid) {
1069         if (key.isNull()) {
1070             if (const char *fpr = sig.fingerprint()) {
1071                 return i18n("Good signature by unknown certificate %1.", QString::fromLatin1(fpr));
1072             } else {
1073                 return i18n("Good signature by an unknown certificate.");
1074             }
1075         } else {
1076             return i18n("Good signature by %1.", nameAndEmailForSummaryLine(key));
1077         }
1078 
1079     } else if (key.isNull()) {
1080         if (const char *fpr = sig.fingerprint()) {
1081             return i18n("Invalid signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status()));
1082         } else {
1083             return i18n("Invalid signature by an unknown certificate: %1", Formatting::errorAsString(sig.status()));
1084         }
1085     } else {
1086         return i18n("Invalid signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status()));
1087     }
1088 }
1089 
1090 //
1091 // ImportResult
1092 //
1093 
1094 QString Formatting::importMetaData(const Import &import, const QStringList &ids)
1095 {
1096     const QString result = importMetaData(import);
1097     if (result.isEmpty()) {
1098         return QString();
1099     } else {
1100         return result + QLatin1Char('\n') + i18n("This certificate was imported from the following sources:") + QLatin1Char('\n') + ids.join(QLatin1Char('\n'));
1101     }
1102 }
1103 
1104 QString Formatting::importMetaData(const Import &import)
1105 {
1106     if (import.isNull()) {
1107         return QString();
1108     }
1109 
1110     if (import.error().isCanceled()) {
1111         return i18n("The import of this certificate was canceled.");
1112     }
1113     if (import.error()) {
1114         return i18n("An error occurred importing this certificate: %1", Formatting::errorAsString(import.error()));
1115     }
1116 
1117     const unsigned int status = import.status();
1118     if (status & Import::NewKey) {
1119         return (status & Import::ContainedSecretKey) ? i18n("This certificate was new to your keystore. The secret key is available.")
1120                                                      : i18n("This certificate is new to your keystore.");
1121     }
1122 
1123     QStringList results;
1124     if (status & Import::NewUserIDs) {
1125         results.push_back(i18n("New user-ids were added to this certificate by the import."));
1126     }
1127     if (status & Import::NewSignatures) {
1128         results.push_back(i18n("New signatures were added to this certificate by the import."));
1129     }
1130     if (status & Import::NewSubkeys) {
1131         results.push_back(i18n("New subkeys were added to this certificate by the import."));
1132     }
1133 
1134     return results.empty() ? i18n("The import contained no new data for this certificate. It is unchanged.") : results.join(QLatin1Char('\n'));
1135 }
1136 
1137 //
1138 // Overview in CertificateDetailsDialog
1139 //
1140 
1141 QString Formatting::formatOverview(const Key &key)
1142 {
1143     return toolTip(key, AllOptions);
1144 }
1145 
1146 QString Formatting::usageString(const Subkey &sub)
1147 {
1148     QStringList usageStrings;
1149     if (sub.canCertify()) {
1150         usageStrings << i18n("Certify");
1151     }
1152     if (sub.canSign()) {
1153         usageStrings << i18n("Sign");
1154     }
1155     if (sub.canEncrypt()) {
1156         usageStrings << i18n("Encrypt");
1157     }
1158     if (sub.canAuthenticate()) {
1159         usageStrings << i18n("Authenticate");
1160     }
1161     if (sub.canRenc()) {
1162         usageStrings << i18nc("Means 'Additional Decryption Subkey'; Don't try translating that, though.", "ADSK");
1163     }
1164     return usageStrings.join(QLatin1StringView(", "));
1165 }
1166 
1167 QString Formatting::summaryLine(const UserID &id)
1168 {
1169     return i18nc("name <email> (validity, protocol, creation date)",
1170                  "%1 (%2, %3, created: %4)",
1171                  nameAndEmailForSummaryLine(id),
1172                  Formatting::complianceStringShort(id),
1173                  displayName(id.parent().protocol()),
1174                  Formatting::creationDateString(id.parent()));
1175 }
1176 
1177 QString Formatting::summaryLine(const Key &key)
1178 {
1179     return nameAndEmailForSummaryLine(key) + QLatin1Char(' ')
1180         + i18nc("(validity, protocol, creation date)",
1181                 "(%1, %2, created: %3)",
1182                 Formatting::complianceStringShort(key),
1183                 displayName(key.protocol()),
1184                 Formatting::creationDateString(key));
1185 }
1186 
1187 QString Formatting::summaryLine(const KeyGroup &group)
1188 {
1189     switch (group.source()) {
1190     case KeyGroup::ApplicationConfig:
1191     case KeyGroup::GnuPGConfig:
1192         return i18ncp("name of group of keys (n key(s), validity)",
1193                       "%2 (1 key, %3)",
1194                       "%2 (%1 keys, %3)",
1195                       group.keys().size(),
1196                       group.name(),
1197                       Formatting::complianceStringShort(group));
1198     case KeyGroup::Tags:
1199         return i18ncp("name of group of keys (n key(s), validity, tag)",
1200                       "%2 (1 key, %3, tag)",
1201                       "%2 (%1 keys, %3, tag)",
1202                       group.keys().size(),
1203                       group.name(),
1204                       Formatting::complianceStringShort(group));
1205     default:
1206         return i18ncp("name of group of keys (n key(s), validity, group ...)",
1207                       "%2 (1 key, %3, unknown origin)",
1208                       "%2 (%1 keys, %3, unknown origin)",
1209                       group.keys().size(),
1210                       group.name(),
1211                       Formatting::complianceStringShort(group));
1212     }
1213 }
1214 
1215 // Icon for certificate selection indication
1216 QIcon Formatting::iconForUid(const UserID &uid)
1217 {
1218     if (Kleo::isRevokedOrExpired(uid)) {
1219         return Formatting::errorIcon();
1220     }
1221     return iconForValidity(uid);
1222 }
1223 
1224 QString Formatting::validity(const UserID &uid)
1225 {
1226     switch (uid.validity()) {
1227     case UserID::Ultimate:
1228         return i18n("The certificate is marked as your own.");
1229     case UserID::Full:
1230         return i18n("The certificate belongs to this recipient.");
1231     case UserID::Marginal:
1232         return i18n("The trust model indicates marginally that the certificate belongs to this recipient.");
1233     case UserID::Never:
1234         return i18n("This certificate should not be used.");
1235     case UserID::Undefined:
1236     case UserID::Unknown:
1237     default:
1238         return i18n("There is no indication that this certificate belongs to this recipient.");
1239     }
1240 }
1241 
1242 QString Formatting::validity(const KeyGroup &group)
1243 {
1244     if (group.isNull()) {
1245         return QString();
1246     }
1247 
1248     const KeyGroup::Keys &keys = group.keys();
1249     if (keys.size() == 0) {
1250         return i18n("This group does not contain any keys.");
1251     }
1252 
1253     return getValidityStatement(keys);
1254 }
1255 
1256 namespace
1257 {
1258 template<typename Container>
1259 UserID::Validity minimalValidity(const Container &keys)
1260 {
1261     const int minValidity = std::accumulate(keys.cbegin(), keys.cend(), UserID::Ultimate + 1, [](int validity, const Key &key) {
1262         return std::min<int>(validity, minimalValidityOfNotRevokedUserIDs(key));
1263     });
1264     return minValidity <= UserID::Ultimate ? static_cast<UserID::Validity>(minValidity) : UserID::Unknown;
1265 }
1266 
1267 template<typename Container>
1268 bool allKeysAreCompliant(const Container &keys)
1269 {
1270     if (!DeVSCompliance::isActive()) {
1271         return true;
1272     }
1273     if (!DeVSCompliance::isCompliant()) {
1274         return false;
1275     }
1276     return Kleo::all_of(keys, DeVSCompliance::keyIsCompliant);
1277 }
1278 }
1279 
1280 QIcon Formatting::validityIcon(const KeyGroup &group)
1281 {
1282     if (Kleo::any_of(group.keys(), std::mem_fn(&Key::isBad))) {
1283         return Formatting::errorIcon();
1284     }
1285     return iconForValidityAndCompliance(minimalValidity(group.keys()), allKeysAreCompliant(group.keys()));
1286 }
1287 
1288 bool Formatting::uidsHaveFullValidity(const Key &key)
1289 {
1290     return allUserIDsHaveFullValidity(key);
1291 }
1292 
1293 QString Formatting::complianceMode()
1294 {
1295     const auto complianceValue = getCryptoConfigStringValue("gpg", "compliance");
1296     return complianceValue == QLatin1StringView("gnupg") ? QString() : complianceValue;
1297 }
1298 
1299 bool Formatting::isKeyDeVs(const GpgME::Key &key)
1300 {
1301     return DeVSCompliance::allSubkeysAreCompliant(key);
1302 }
1303 
1304 QString Formatting::complianceStringForKey(const GpgME::Key &key)
1305 {
1306     // There will likely be more in the future for other institutions
1307     // for now we only have DE-VS
1308     if (DeVSCompliance::isCompliant()) {
1309         return isRemoteKey(key) //
1310             ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown")
1311             : DeVSCompliance::name(DeVSCompliance::keyIsCompliant(key));
1312     }
1313     return QString();
1314 }
1315 
1316 QString Formatting::complianceStringForUserID(const GpgME::UserID &userID)
1317 {
1318     // There will likely be more in the future for other institutions
1319     // for now we only have DE-VS
1320     if (DeVSCompliance::isCompliant()) {
1321         return isRemoteKey(userID.parent()) //
1322             ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown")
1323             : DeVSCompliance::name(DeVSCompliance::userIDIsCompliant(userID));
1324     }
1325     return QString();
1326 }
1327 
1328 QString Formatting::complianceStringShort(const GpgME::UserID &id)
1329 {
1330     if (DeVSCompliance::isCompliant() && DeVSCompliance::userIDIsCompliant(id)) {
1331         return QStringLiteral("★ ") + DeVSCompliance::name(true);
1332     }
1333     const bool keyValidityChecked = (id.parent().keyListMode() & GpgME::Validate);
1334     if (keyValidityChecked && id.validity() >= UserID::Full) {
1335         return i18nc("As in 'this user ID is valid.'", "certified");
1336     }
1337     if (id.parent().isExpired() || isExpired(id)) {
1338         return i18n("expired");
1339     }
1340     if (id.parent().isRevoked() || id.isRevoked()) {
1341         return i18n("revoked");
1342     }
1343     if (id.parent().isDisabled()) {
1344         return i18n("disabled");
1345     }
1346     if (id.parent().isInvalid() || id.isInvalid()) {
1347         return i18n("invalid");
1348     }
1349     if (keyValidityChecked) {
1350         return i18nc("As in 'this user ID is not certified'", "not certified");
1351     }
1352 
1353     return i18nc("The validity of this user ID has not been/could not be checked", "not checked");
1354 }
1355 
1356 QString Formatting::complianceStringShort(const GpgME::Key &key)
1357 {
1358     if (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(key)) {
1359         return QStringLiteral("★ ") + DeVSCompliance::name(true);
1360     }
1361     const bool keyValidityChecked = (key.keyListMode() & GpgME::Validate);
1362     if (keyValidityChecked && Kleo::allUserIDsHaveFullValidity(key)) {
1363         return i18nc("As in all user IDs are valid.", "certified");
1364     }
1365     if (key.isExpired()) {
1366         return i18n("expired");
1367     }
1368     if (key.isRevoked()) {
1369         return i18n("revoked");
1370     }
1371     if (key.isDisabled()) {
1372         return i18n("disabled");
1373     }
1374     if (key.isInvalid()) {
1375         return i18n("invalid");
1376     }
1377     if (keyValidityChecked) {
1378         return i18nc("As in not all user IDs are valid.", "not certified");
1379     }
1380 
1381     return i18nc("The validity of the user IDs has not been/could not be checked", "not checked");
1382 }
1383 
1384 QString Formatting::complianceStringShort(const KeyGroup &group)
1385 {
1386     const KeyGroup::Keys &keys = group.keys();
1387 
1388     const bool allKeysFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Kleo::allUserIDsHaveFullValidity);
1389     if (allKeysFullyValid) {
1390         return i18nc("As in all keys are valid.", "all certified");
1391     }
1392 
1393     return i18nc("As in not all keys are valid.", "not all certified");
1394 }
1395 
1396 QString Formatting::prettyID(const char *id)
1397 {
1398     if (!id) {
1399         return QString();
1400     }
1401     QString ret = QString::fromLatin1(id).toUpper().replace(QRegularExpression(QStringLiteral("(....)")), QStringLiteral("\\1 ")).trimmed();
1402     // For the standard 10 group fingerprint let us use a double space in the
1403     // middle to increase readability
1404     if (ret.size() == 49) {
1405         ret.insert(24, QLatin1Char(' '));
1406     }
1407     return ret;
1408 }
1409 
1410 QString Formatting::accessibleHexID(const char *id)
1411 {
1412     static const QRegularExpression groupOfFourRegExp{QStringLiteral("(?:(.)(.)(.)(.))")};
1413 
1414     QString ret;
1415     ret = QString::fromLatin1(id);
1416     if (!ret.isEmpty() && (ret.size() % 4 == 0)) {
1417         ret = ret.replace(groupOfFourRegExp, QStringLiteral("\\1 \\2 \\3 \\4, ")).chopped(2);
1418     }
1419     return ret;
1420 }
1421 
1422 QString Formatting::origin(int o)
1423 {
1424     switch (o) {
1425     case Key::OriginKS:
1426         return i18n("Keyserver");
1427     case Key::OriginDane:
1428         return QStringLiteral("DANE");
1429     case Key::OriginWKD:
1430         return QStringLiteral("WKD");
1431     case Key::OriginURL:
1432         return QStringLiteral("URL");
1433     case Key::OriginFile:
1434         return i18n("File import");
1435     case Key::OriginSelf:
1436         return i18n("Generated");
1437     case Key::OriginOther:
1438     case Key::OriginUnknown:
1439     default:
1440         return i18n("Unknown");
1441     }
1442 }
1443 
1444 QString Formatting::deVsString(bool compliant)
1445 {
1446     return DeVSCompliance::name(compliant);
1447 }
1448 
1449 namespace
1450 {
1451 QString formatTrustScope(const char *trustScope)
1452 {
1453     static const QRegularExpression escapedNonAlphaNum{QStringLiteral(R"(\\([^0-9A-Za-z]))")};
1454 
1455     const auto scopeRegExp = QString::fromUtf8(trustScope);
1456     if (scopeRegExp.startsWith(u"<[^>]+[@.]") && scopeRegExp.endsWith(u">$")) {
1457         // looks like a trust scope regular expression created by gpg
1458         auto domain = scopeRegExp.mid(10, scopeRegExp.size() - 10 - 2);
1459         domain.replace(escapedNonAlphaNum, QStringLiteral(R"(\1)"));
1460         return domain;
1461     }
1462     return scopeRegExp;
1463 }
1464 }
1465 
1466 QString Formatting::trustSignatureDomain(const GpgME::UserID::Signature &sig)
1467 {
1468     return formatTrustScope(sig.trustScope());
1469 }
1470 
1471 QString Formatting::trustSignature(const GpgME::UserID::Signature &sig)
1472 {
1473     switch (sig.trustValue()) {
1474     case TrustSignatureTrust::Partial:
1475         return i18nc("Certifies this key as partially trusted introducer for 'domain name'.",
1476                      "Certifies this key as partially trusted introducer for '%1'.",
1477                      trustSignatureDomain(sig));
1478     case TrustSignatureTrust::Complete:
1479         return i18nc("Certifies this key as fully trusted introducer for 'domain name'.",
1480                      "Certifies this key as fully trusted introducer for '%1'.",
1481                      trustSignatureDomain(sig));
1482     default:
1483         return {};
1484     }
1485 }
1486 
1487 QString Formatting::errorAsString(const GpgME::Error &error)
1488 {
1489 #ifdef Q_OS_WIN
1490     // On Windows, we set GpgME resp. libgpg-error to return (translated) error messages as UTF-8
1491     const char *s = error.asString();
1492     qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1);
1493     qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s;
1494     qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray{s}.toPercentEncoding();
1495     return QString::fromUtf8(s);
1496 #else
1497     return QString::fromLocal8Bit(error.asString());
1498 #endif
1499 }
1500 
1501 QString Formatting::prettyAlgorithmName(const std::string &algorithm)
1502 {
1503     static const std::map<std::string, QString> displayNames = {
1504         {"brainpoolP256r1", i18nc("@info", "ECC (Brainpool P-256)")},
1505         {"brainpoolP384r1", i18nc("@info", "ECC (Brainpool P-384)")},
1506         {"brainpoolP512r1", i18nc("@info", "ECC (Brainpool P-512)")},
1507         {"curve25519", i18nc("@info", "ECC (Curve25519)")},
1508         {"curve448", i18nc("@info", "ECC (Curve448)")},
1509         {"ed25519", i18nc("@info", "ECC (Ed25519)")},
1510         {"ed448", i18nc("@info", "ECC (Ed448)")},
1511         {"cv25519", i18nc("@info", "ECC (Cv25519)")},
1512         {"cv448", i18nc("@info", "ECC (Cv448)")},
1513         {"nistp256", i18nc("@info", "ECC (NIST P-256)")},
1514         {"nistp384", i18nc("@info", "ECC (NIST P-384)")},
1515         {"nistp521", i18nc("@info", "ECC (NIST P-521)")},
1516         {"rsa2048", i18nc("@info", "RSA 2048")},
1517         {"rsa3072", i18nc("@info", "RSA 3072")},
1518         {"rsa4096", i18nc("@info", "RSA 4096")},
1519         {"dsa1024", i18nc("@info", "DSA 1024")},
1520         {"dsa2048", i18nc("@info", "DSA 2048")},
1521         {"elg1024", i18nc("@info", "Elgamal 1024")},
1522         {"elg2048", i18nc("@info", "Elgamal 2048")},
1523         {"elg3072", i18nc("@info", "Elgamal 3072")},
1524         {"elg4096", i18nc("@info", "Elgamal 4096")},
1525     };
1526     const auto it = displayNames.find(algorithm);
1527     return (it != displayNames.end()) ? it->second : i18nc("@info", "Unknown algorithm");
1528 }