1 /* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*-
2     utils/formatting.cpp
3 
4     This file is part of Kleopatra, the KDE keymanager
5     SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB
6     SPDX-FileCopyrightText: 2021 g10 Code GmbH
7     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
8 
9     SPDX-License-Identifier: GPL-2.0-or-later
10 */
11 
12 #include <config-libkleo.h>
13 
14 #include "formatting.h"
15 #include "kleo/dn.h"
16 #include "kleo/keyfiltermanager.h"
17 #include "kleo/keygroup.h"
18 
19 #include "utils/cryptoconfig.h"
20 #include "utils/gnupg.h"
21 
22 #include <gpgme++/key.h>
23 #include <gpgme++/importresult.h>
24 
25 #include <QGpgME/CryptoConfig>
26 #include <QGpgME/Protocol>
27 
28 #include <KLocalizedString>
29 #include <KEmailAddress>
30 
31 #include <QString>
32 #include <QDateTime>
33 #include <QTextDocument> // for Qt::escape
34 #include <QLocale>
35 #include <QIcon>
36 #include <QRegularExpression>
37 
38 #include "models/keycache.h"
39 
40 using namespace GpgME;
41 using namespace Kleo;
42 
43 //
44 // Name
45 //
46 
prettyName(int proto,const char * id,const char * name_,const char * comment_)47 QString Formatting::prettyName(int proto, const char *id, const char *name_, const char *comment_)
48 {
49 
50     if (proto == OpenPGP) {
51         const QString name = QString::fromUtf8(name_);
52         if (name.isEmpty()) {
53             return QString();
54         }
55         const QString comment = QString::fromUtf8(comment_);
56         if (comment.isEmpty()) {
57             return name;
58         }
59         return QStringLiteral("%1 (%2)").arg(name, comment);
60     }
61 
62     if (proto == CMS) {
63         const DN subject(id);
64         const QString cn = subject[QStringLiteral("CN")].trimmed();
65         if (cn.isEmpty()) {
66             return subject.prettyDN();
67         }
68         return cn;
69     }
70 
71     return QString();
72 }
73 
prettyNameAndEMail(int proto,const char * id,const char * name_,const char * email_,const char * comment_)74 QString Formatting::prettyNameAndEMail(int proto, const char *id, const char *name_, const char *email_, const char *comment_)
75 {
76     return prettyNameAndEMail(proto, QString::fromUtf8(id), QString::fromUtf8(name_), prettyEMail(email_, id), QString::fromUtf8(comment_));
77 }
78 
prettyNameAndEMail(int proto,const QString & id,const QString & name,const QString & email,const QString & comment)79 QString Formatting::prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment)
80 {
81 
82     if (proto == OpenPGP) {
83         if (name.isEmpty()) {
84             if (email.isEmpty()) {
85                 return QString();
86             } else if (comment.isEmpty()) {
87                 return QStringLiteral("<%1>").arg(email);
88             } else {
89                 return QStringLiteral("(%2) <%1>").arg(email, comment);
90             }
91         }
92         if (email.isEmpty()) {
93             if (comment.isEmpty()) {
94                 return name;
95             } else {
96                 return QStringLiteral("%1 (%2)").arg(name, comment);
97             }
98         }
99         if (comment.isEmpty()) {
100             return QStringLiteral("%1 <%2>").arg(name, email);
101         } else {
102             return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment);
103         }
104     }
105 
106     if (proto == CMS) {
107         const DN subject(id);
108         const QString cn = subject[QStringLiteral("CN")].trimmed();
109         if (cn.isEmpty()) {
110             return subject.prettyDN();
111         }
112         return cn;
113     }
114     return QString();
115 }
116 
prettyUserID(const UserID & uid)117 QString Formatting::prettyUserID(const UserID &uid)
118 {
119     if (uid.parent().protocol() == OpenPGP) {
120         return prettyNameAndEMail(uid);
121     }
122     const QByteArray id = QByteArray(uid.id()).trimmed();
123     if (id.startsWith('<')) {
124         return prettyEMail(uid.email(), uid.id());
125     }
126     if (id.startsWith('('))
127         // ### parse uri/dns:
128     {
129         return QString::fromUtf8(uid.id());
130     } else {
131         return DN(uid.id()).prettyDN();
132     }
133 }
134 
prettyKeyID(const char * id)135 QString Formatting::prettyKeyID(const char *id)
136 {
137     if (!id) {
138         return QString();
139     }
140     return QLatin1String("0x") + QString::fromLatin1(id).toUpper();
141 }
142 
prettyNameAndEMail(const UserID & uid)143 QString Formatting::prettyNameAndEMail(const UserID &uid)
144 {
145     return prettyNameAndEMail(uid.parent().protocol(), uid.id(), uid.name(), uid.email(), uid.comment());
146 }
147 
prettyNameAndEMail(const Key & key)148 QString Formatting::prettyNameAndEMail(const Key &key)
149 {
150     return prettyNameAndEMail(key.userID(0));
151 }
152 
prettyName(const Key & key)153 QString Formatting::prettyName(const Key &key)
154 {
155     return prettyName(key.userID(0));
156 }
157 
prettyName(const UserID & uid)158 QString Formatting::prettyName(const UserID &uid)
159 {
160     return prettyName(uid.parent().protocol(), uid.id(), uid.name(), uid.comment());
161 }
162 
prettyName(const UserID::Signature & sig)163 QString Formatting::prettyName(const UserID::Signature &sig)
164 {
165     return prettyName(OpenPGP, sig.signerUserID(), sig.signerName(), sig.signerComment());
166 }
167 
168 //
169 // EMail
170 //
171 
prettyEMail(const Key & key)172 QString Formatting::prettyEMail(const Key &key)
173 {
174     for (unsigned int i = 0, end = key.numUserIDs(); i < end; ++i) {
175         const QString email = prettyEMail(key.userID(i));
176         if (!email.isEmpty()) {
177             return email;
178         }
179     }
180     return QString();
181 }
182 
prettyEMail(const UserID & uid)183 QString Formatting::prettyEMail(const UserID &uid)
184 {
185     return prettyEMail(uid.email(), uid.id());
186 }
187 
prettyEMail(const UserID::Signature & sig)188 QString Formatting::prettyEMail(const UserID::Signature &sig)
189 {
190     return prettyEMail(sig.signerEmail(), sig.signerUserID());
191 }
192 
prettyEMail(const char * email_,const char * id)193 QString Formatting::prettyEMail(const char *email_, const char *id)
194 {
195     QString email;
196     QString name;
197     QString comment;
198     if (email_ && KEmailAddress::splitAddress(QString::fromUtf8(email_),
199                                               name, email, comment) == KEmailAddress::AddressOk) {
200         return email;
201     } else {
202         return DN(id)[QStringLiteral("EMAIL")].trimmed();
203     }
204 }
205 
206 //
207 // Tooltip
208 //
209 
210 namespace
211 {
212 
protect_whitespace(QString s)213 static QString protect_whitespace(QString s)
214 {
215     static const QLatin1Char SP(' ');
216     static const QLatin1Char NBSP('\xA0');
217     return s.replace(SP, NBSP);
218 }
219 
220 template <typename T_arg>
format_row(const QString & field,const T_arg & arg)221 QString format_row(const QString &field, const T_arg &arg)
222 {
223     return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").arg(protect_whitespace(field), arg);
224 }
format_row(const QString & field,const QString & arg)225 QString format_row(const QString &field, const QString &arg)
226 {
227     return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").arg(protect_whitespace(field), arg.toHtmlEscaped());
228 }
format_row(const QString & field,const char * arg)229 QString format_row(const QString &field, const char *arg)
230 {
231     return format_row(field, QString::fromUtf8(arg));
232 }
233 
format_keytype(const Key & key)234 QString format_keytype(const Key &key)
235 {
236     const Subkey subkey = key.subkey(0);
237     if (key.hasSecret()) {
238         return i18n("%1-bit %2 (secret key available)", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString()));
239     } else {
240         return i18n("%1-bit %2", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString()));
241     }
242 }
243 
format_subkeytype(const Subkey & subkey)244 QString format_subkeytype(const Subkey &subkey)
245 {
246     const auto algo = subkey.publicKeyAlgorithm();
247 
248     if (algo == Subkey::AlgoECC ||
249         algo == Subkey::AlgoECDSA ||
250         algo == Subkey::AlgoECDH ||
251         algo == Subkey::AlgoEDDSA) {
252         return QString::fromStdString(subkey.algoName());
253     }
254     return i18n("%1-bit %2", subkey.length(), QLatin1String(subkey.publicKeyAlgorithmAsString()));
255 }
256 
format_keyusage(const Key & key)257 QString format_keyusage(const Key &key)
258 {
259     QStringList capabilities;
260     if (key.canReallySign()) {
261         if (key.isQualified()) {
262             capabilities.push_back(i18n("Signing (Qualified)"));
263         } else {
264             capabilities.push_back(i18n("Signing"));
265         }
266     }
267     if (key.canEncrypt()) {
268         capabilities.push_back(i18n("Encryption"));
269     }
270     if (key.canCertify()) {
271         capabilities.push_back(i18n("Certifying User-IDs"));
272     }
273     if (key.canAuthenticate()) {
274         capabilities.push_back(i18n("SSH Authentication"));
275     }
276     return capabilities.join(QLatin1String(", "));
277 }
278 
format_subkeyusage(const Subkey & subkey)279 QString format_subkeyusage(const Subkey &subkey)
280 {
281     QStringList capabilities;
282     if (subkey.canSign()) {
283         if (subkey.isQualified()) {
284             capabilities.push_back(i18n("Signing (Qualified)"));
285         } else {
286             capabilities.push_back(i18n("Signing"));
287         }
288     }
289     if (subkey.canEncrypt()) {
290         capabilities.push_back(i18n("Encryption"));
291     }
292     if (subkey.canCertify()) {
293         capabilities.push_back(i18n("Certifying User-IDs"));
294     }
295     if (subkey.canAuthenticate()) {
296         capabilities.push_back(i18n("SSH Authentication"));
297     }
298     return capabilities.join(QLatin1String(", "));
299 }
300 
time_t2string(time_t t)301 static QString time_t2string(time_t t)
302 {
303     QDateTime dt;
304     dt.setTime_t(t);
305     return QLocale().toString(dt, QLocale::ShortFormat);
306 }
307 
make_red(const QString & txt)308 static QString make_red(const QString &txt)
309 {
310     return QLatin1String("<font color=\"red\">") + txt.toHtmlEscaped() + QLatin1String("</font>");
311 }
312 
313 }
314 
toolTip(const Key & key,int flags)315 QString Formatting::toolTip(const Key &key, int flags)
316 {
317     if (flags == 0 || (key.protocol() != CMS && key.protocol() != OpenPGP)) {
318         return QString();
319     }
320 
321     const Subkey subkey = key.subkey(0);
322 
323     QString result;
324     if (flags & Validity) {
325         if (key.protocol() == OpenPGP || (key.keyListMode() & Validate)) {
326             if (key.isRevoked()) {
327                 result = make_red(i18n("Revoked"));
328             } else if (key.isExpired()) {
329                 result = make_red(i18n("Expired"));
330             } else if (key.isDisabled()) {
331                 result = i18n("Disabled");
332             } else if (key.keyListMode() & GpgME::Validate) {
333                 unsigned int fullyTrusted = 0;
334                 for (const auto &uid: key.userIDs()) {
335                     if (uid.validity() >= UserID::Validity::Full) {
336                         fullyTrusted++;
337                     }
338                 }
339                 if (fullyTrusted == key.numUserIDs()) {
340                     result = i18n("All User-IDs are certified.");
341                     const auto compliance = complianceStringForKey(key);
342                     if (!compliance.isEmpty()) {
343                         result += QStringLiteral("<br>") + compliance;
344                     }
345                 } else {
346                     result = i18np("One User-ID is not certified.", "%1 User-IDs are not certified.", key.numUserIDs() - fullyTrusted);
347                 }
348             } else {
349                 result = i18n("The validity cannot be checked at the moment.");
350             }
351         } else {
352             result = i18n("The validity cannot be checked at the moment.");
353         }
354     }
355     if (flags == Validity) {
356         return result;
357     }
358 
359     result += QLatin1String("<table border=\"0\">");
360     if (key.protocol() == CMS) {
361         if (flags & SerialNumber) {
362             result += format_row(i18n("Serial number"), key.issuerSerial());
363         }
364         if (flags & Issuer) {
365             result += format_row(i18n("Issuer"), key.issuerName());
366         }
367     }
368     if (flags & UserIDs) {
369         const std::vector<UserID> uids = key.userIDs();
370         if (!uids.empty()) {
371             result += format_row(key.protocol() == CMS
372                                  ? i18n("Subject")
373                                  : i18n("User-ID"), prettyUserID(uids.front()));
374         }
375         if (uids.size() > 1) {
376             for (auto it = uids.begin() + 1, end = uids.end(); it != end; ++it) {
377                 if (!it->isRevoked() && !it->isInvalid()) {
378                     result += format_row(i18n("a.k.a."), prettyUserID(*it));
379                 }
380             }
381         }
382     }
383     if (flags & ExpiryDates) {
384         result += format_row(i18n("Created"), time_t2string(subkey.creationTime()));
385 
386         if (key.isExpired()) {
387             result += format_row(i18n("Expired"), time_t2string(subkey.expirationTime()));
388         } else if (!subkey.neverExpires()) {
389             result += format_row(i18n("Expires"), time_t2string(subkey.expirationTime()));
390         }
391     }
392     if (flags & CertificateType) {
393         result += format_row(i18n("Type"), format_keytype(key));
394     }
395     if (flags & CertificateUsage) {
396         result += format_row(i18n("Usage"), format_keyusage(key));
397     }
398     if (flags & KeyID) {
399         result += format_row(i18n("Key-ID"), QString::fromLatin1(key.shortKeyID()));
400     }
401     if (flags & Fingerprint) {
402         result += format_row(i18n("Fingerprint"), key.primaryFingerprint());
403     }
404     if (flags & OwnerTrust) {
405         if (key.protocol() == OpenPGP) {
406             result += format_row(i18n("Certification trust"), ownerTrustShort(key));
407         } else if (key.isRoot()) {
408             result += format_row(i18n("Trusted issuer?"),
409                                  key.userID(0).validity() == UserID::Ultimate ? i18n("Yes") :
410                                  /* else */                                     i18n("No"));
411         }
412     }
413 
414     if (flags & StorageLocation) {
415         if (const char *card = subkey.cardSerialNumber()) {
416             result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card)));
417         } else {
418             result += format_row(i18n("Stored"), i18nc("stored...", "on this computer"));
419         }
420     }
421     if (flags & Subkeys) {
422         for (const auto &sub: key.subkeys()) {
423             result += QLatin1String("<hr/>");
424             result += format_row(i18n("Subkey"), sub.fingerprint());
425             if (sub.isRevoked()) {
426                 result += format_row(i18n("Status"), i18n("Revoked"));
427             } else if (sub.isExpired()) {
428                 result += format_row(i18n("Status"), i18n("Expired"));
429             }
430             if (flags & ExpiryDates) {
431                 result += format_row(i18n("Created"), time_t2string(sub.creationTime()));
432 
433                 if (key.isExpired()) {
434                     result += format_row(i18n("Expired"), time_t2string(sub.expirationTime()));
435                 } else if (!subkey.neverExpires()) {
436                     result += format_row(i18n("Expires"), time_t2string(sub.expirationTime()));
437                 }
438             }
439 
440             if (flags & CertificateType) {
441                 result += format_row(i18n("Type"), format_subkeytype(sub));
442             }
443             if (flags & CertificateUsage) {
444                 result += format_row(i18n("Usage"), format_subkeyusage(sub));
445             }
446             if (flags & StorageLocation) {
447                 if (const char *card = sub.cardSerialNumber()) {
448                     result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card)));
449                 } else {
450                     result += format_row(i18n("Stored"), i18nc("stored...", "on this computer"));
451                 }
452             }
453         }
454     }
455     result += QLatin1String("</table>");
456 
457     return result;
458 }
459 
460 namespace
461 {
462 template <typename Container>
getValidityStatement(const Container & keys)463 QString getValidityStatement(const Container &keys)
464 {
465     const bool allKeysAreOpenPGP = std::all_of(keys.cbegin(), keys.cend(), [] (const Key &key) { return key.protocol() == OpenPGP; });
466     const bool allKeysAreValidated = std::all_of(keys.cbegin(), keys.cend(), [] (const Key &key) { return key.keyListMode() & Validate; });
467     if (allKeysAreOpenPGP || allKeysAreValidated) {
468         const bool someKeysAreBad = std::any_of(keys.cbegin(), keys.cend(), std::mem_fn(&Key::isBad));
469         if (someKeysAreBad) {
470             return i18n("Some keys are revoked, expired, disabled, or invalid.");
471         } else {
472             const bool allKeysAreFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Formatting::uidsHaveFullValidity);
473             if (allKeysAreFullyValid) {
474                 return i18n("All keys are certified.");
475             } else {
476                 return i18n("Some keys are not certified.");
477             }
478         }
479     }
480     return i18n("The validity of the keys cannot be checked at the moment.");
481 }
482 }
483 
toolTip(const KeyGroup & group,int flags)484 QString Formatting::toolTip(const KeyGroup &group, int flags)
485 {
486     static const unsigned int maxNumKeysForTooltip = 20;
487 
488     if (group.isNull()) {
489         return QString();
490     }
491 
492     const KeyGroup::Keys &keys = group.keys();
493     if (keys.size() == 0) {
494         return i18nc("@info:tooltip", "This group does not contain any keys.");
495     }
496 
497     const QString validity = (flags & Validity) ? getValidityStatement(keys) : QString();
498     if (flags == Validity) {
499         return validity;
500     }
501 
502     // list either up to maxNumKeysForTooltip keys or (maxNumKeysForTooltip-1) keys followed by "and n more keys"
503     const unsigned int numKeysForTooltip = keys.size() > maxNumKeysForTooltip ? maxNumKeysForTooltip - 1 : keys.size();
504 
505     QStringList result;
506     result.reserve(3 + 2 + numKeysForTooltip + 2);
507     if (!validity.isEmpty()) {
508         result.push_back(QStringLiteral("<p>"));
509         result.push_back(validity.toHtmlEscaped());
510         result.push_back(QStringLiteral("</p>"));
511     }
512 
513     result.push_back(QStringLiteral("<p>"));
514     result.push_back(i18n("Keys:"));
515     {
516         auto it = keys.cbegin();
517         for (unsigned int i = 0; i < numKeysForTooltip; ++i, ++it) {
518             result.push_back(QLatin1String("<br>") + Formatting::summaryLine(*it).toHtmlEscaped());
519         }
520     }
521     if (keys.size() > numKeysForTooltip) {
522         result.push_back(QLatin1String("<br>") + i18ncp("this follows a list of keys", "and 1 more key", "and %1 more keys", keys.size() - numKeysForTooltip));
523     }
524     result.push_back(QStringLiteral("</p>"));
525 
526     return result.join(QLatin1Char('\n'));
527 }
528 
529 //
530 // Creation and Expiration
531 //
532 
533 namespace
534 {
time_t2date(time_t t)535 static QDate time_t2date(time_t t)
536 {
537     if (!t) {
538         return {};
539     }
540     QDateTime dt;
541     dt.setTime_t(t);
542     return dt.date();
543 }
date2string(const QDate & date)544 static QString date2string(const QDate &date)
545 {
546     return QLocale().toString(date, QLocale::ShortFormat);
547 }
548 
549 template <typename T>
expiration_date_string(const T & tee)550 QString expiration_date_string(const T &tee)
551 {
552     return tee.neverExpires() ? QString() : date2string(time_t2date(tee.expirationTime()));
553 }
554 template <typename T>
creation_date(const T & tee)555 QDate creation_date(const T &tee)
556 {
557     return time_t2date(tee.creationTime());
558 }
559 template <typename T>
expiration_date(const T & tee)560 QDate expiration_date(const T &tee)
561 {
562     return time_t2date(tee.expirationTime());
563 }
564 }
565 
dateString(time_t t)566 QString Formatting::dateString(time_t t)
567 {
568     return date2string(time_t2date(t));
569 }
570 
expirationDateString(const Key & key)571 QString Formatting::expirationDateString(const Key &key)
572 {
573     return expiration_date_string(key.subkey(0));
574 }
575 
expirationDateString(const Subkey & subkey)576 QString Formatting::expirationDateString(const Subkey &subkey)
577 {
578     return expiration_date_string(subkey);
579 }
580 
expirationDateString(const UserID::Signature & sig)581 QString Formatting::expirationDateString(const UserID::Signature &sig)
582 {
583     return expiration_date_string(sig);
584 }
585 
expirationDate(const Key & key)586 QDate Formatting::expirationDate(const Key &key)
587 {
588     return expiration_date(key.subkey(0));
589 }
590 
expirationDate(const Subkey & subkey)591 QDate Formatting::expirationDate(const Subkey &subkey)
592 {
593     return expiration_date(subkey);
594 }
595 
expirationDate(const UserID::Signature & sig)596 QDate Formatting::expirationDate(const UserID::Signature &sig)
597 {
598     return expiration_date(sig);
599 }
600 
creationDateString(const Key & key)601 QString Formatting::creationDateString(const Key &key)
602 {
603     return date2string(creation_date(key.subkey(0)));
604 }
605 
creationDateString(const Subkey & subkey)606 QString Formatting::creationDateString(const Subkey &subkey)
607 {
608     return date2string(creation_date(subkey));
609 }
610 
creationDateString(const UserID::Signature & sig)611 QString Formatting::creationDateString(const UserID::Signature &sig)
612 {
613     return date2string(creation_date(sig));
614 }
615 
creationDate(const Key & key)616 QDate Formatting::creationDate(const Key &key)
617 {
618     return creation_date(key.subkey(0));
619 }
620 
creationDate(const Subkey & subkey)621 QDate Formatting::creationDate(const Subkey &subkey)
622 {
623     return creation_date(subkey);
624 }
625 
creationDate(const UserID::Signature & sig)626 QDate Formatting::creationDate(const UserID::Signature &sig)
627 {
628     return creation_date(sig);
629 }
630 
631 //
632 // Types
633 //
634 
displayName(Protocol p)635 QString Formatting::displayName(Protocol p)
636 {
637     if (p == CMS) {
638         return i18nc("X.509/CMS encryption standard", "S/MIME");
639     }
640     if (p == OpenPGP) {
641         return i18n("OpenPGP");
642     }
643     return i18nc("Unknown encryption protocol", "Unknown");
644 }
645 
type(const Key & key)646 QString Formatting::type(const Key &key)
647 {
648     return displayName(key.protocol());
649 }
650 
type(const Subkey & subkey)651 QString Formatting::type(const Subkey &subkey)
652 {
653     return QString::fromUtf8(subkey.publicKeyAlgorithmAsString());
654 }
655 
type(const KeyGroup & group)656 QString Formatting::type(const KeyGroup &group)
657 {
658     Q_UNUSED(group)
659     return i18nc("a group of keys/certificates", "Group");
660 }
661 
662 //
663 // Status / Validity
664 //
665 
ownerTrustShort(const Key & key)666 QString Formatting::ownerTrustShort(const Key &key)
667 {
668     return ownerTrustShort(key.ownerTrust());
669 }
670 
ownerTrustShort(Key::OwnerTrust trust)671 QString Formatting::ownerTrustShort(Key::OwnerTrust trust)
672 {
673     switch (trust) {
674     case Key::Unknown:   return i18nc("unknown trust level", "unknown");
675     case Key::Never:     return i18n("untrusted");
676     case Key::Marginal:  return i18nc("marginal trust", "marginal");
677     case Key::Full:      return i18nc("full trust", "full");
678     case Key::Ultimate:  return i18nc("ultimate trust", "ultimate");
679     case Key::Undefined: return i18nc("undefined trust", "undefined");
680     default:
681         Q_ASSERT(!"unexpected owner trust value");
682         break;
683     }
684     return QString();
685 }
686 
validityShort(const Subkey & subkey)687 QString Formatting::validityShort(const Subkey &subkey)
688 {
689     if (subkey.isRevoked()) {
690         return i18n("revoked");
691     }
692     if (subkey.isExpired()) {
693         return i18n("expired");
694     }
695     if (subkey.isDisabled()) {
696         return i18n("disabled");
697     }
698     if (subkey.isInvalid()) {
699         return i18n("invalid");
700     }
701     return i18nc("as in good/valid signature", "good");
702 }
703 
validityShort(const UserID & uid)704 QString Formatting::validityShort(const UserID &uid)
705 {
706     if (uid.isRevoked()) {
707         return i18n("revoked");
708     }
709     if (uid.isInvalid()) {
710         return i18n("invalid");
711     }
712     switch (uid.validity()) {
713     case UserID::Unknown:   return i18nc("unknown trust level", "unknown");
714     case UserID::Undefined: return i18nc("undefined trust", "undefined");
715     case UserID::Never:     return i18n("untrusted");
716     case UserID::Marginal:  return i18nc("marginal trust", "marginal");
717     case UserID::Full:      return i18nc("full trust", "full");
718     case UserID::Ultimate:  return i18nc("ultimate trust", "ultimate");
719     }
720     return QString();
721 }
722 
validityShort(const UserID::Signature & sig)723 QString Formatting::validityShort(const UserID::Signature &sig)
724 {
725     switch (sig.status()) {
726     case UserID::Signature::NoError:
727         if (!sig.isInvalid()) {
728             /* See RFC 4880 Section 5.2.1 */
729             switch (sig.certClass()) {
730             case 0x10: /* Generic */
731             case 0x11: /* Persona */
732             case 0x12: /* Casual */
733             case 0x13: /* Positive */
734                 return i18n("valid");
735             case 0x30:
736                 return i18n("revoked");
737             default:
738                 return i18n("class %1", sig.certClass());
739             }
740         }
741         Q_FALLTHROUGH();
742         // fall through:
743     case UserID::Signature::GeneralError:
744         return i18n("invalid");
745     case UserID::Signature::SigExpired:   return i18n("expired");
746     case UserID::Signature::KeyExpired:   return i18n("certificate expired");
747     case UserID::Signature::BadSignature: return i18nc("fake/invalid signature", "bad");
748     case UserID::Signature::NoPublicKey: {
749             /* GnuPG returns the same error for no public key as for expired
750              * or revoked certificates. */
751             const auto key = KeyCache::instance()->findByKeyIDOrFingerprint (sig.signerKeyID());
752             if (key.isNull()) {
753                 return i18n("no public key");
754             } else if (key.isExpired()) {
755                 return i18n("key expired");
756             } else if (key.isRevoked()) {
757                 return i18n("key revoked");
758             } else if (key.isDisabled()) {
759                 return i18n("key disabled");
760             }
761             /* can't happen */
762             return QStringLiteral("unknown");
763         }
764     }
765     return QString();
766 }
767 
validityIcon(const UserID::Signature & sig)768 QIcon Formatting::validityIcon(const UserID::Signature &sig)
769 {
770     switch (sig.status()) {
771     case UserID::Signature::NoError:
772         if (!sig.isInvalid()) {
773             /* See RFC 4880 Section 5.2.1 */
774             switch (sig.certClass()) {
775             case 0x10: /* Generic */
776             case 0x11: /* Persona */
777             case 0x12: /* Casual */
778             case 0x13: /* Positive */
779                 return QIcon::fromTheme(QStringLiteral("emblem-success"));
780             case 0x30:
781                 return QIcon::fromTheme(QStringLiteral("emblem-error"));
782             default:
783                 return QIcon();
784             }
785         }
786         Q_FALLTHROUGH();
787         // fall through:
788     case UserID::Signature::BadSignature:
789     case UserID::Signature::GeneralError:
790         return QIcon::fromTheme(QStringLiteral("emblem-error"));
791     case UserID::Signature::SigExpired:
792     case UserID::Signature::KeyExpired:
793         return QIcon::fromTheme(QStringLiteral("emblem-information"));
794     case UserID::Signature::NoPublicKey:
795         return QIcon::fromTheme(QStringLiteral("emblem-question"));
796     }
797     return QIcon();
798 }
799 
formatKeyLink(const Key & key)800 QString Formatting::formatKeyLink(const Key &key)
801 {
802     if (key.isNull()) {
803         return QString();
804     }
805     return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(QLatin1String(key.primaryFingerprint()), Formatting::prettyName(key));
806 }
807 
formatForComboBox(const GpgME::Key & key)808 QString Formatting::formatForComboBox(const GpgME::Key &key)
809 {
810     const QString name = prettyName(key);
811     QString mail = prettyEMail(key);
812     if (!mail.isEmpty()) {
813         mail = QLatin1Char('<') + mail + QLatin1Char('>');
814     }
815     return i18nc("name, email, key id", "%1 %2 (%3)", name, mail, QLatin1String(key.shortKeyID())).simplified();
816 }
817 
818 namespace
819 {
820 
keyToString(const Key & key)821 static QString keyToString(const Key &key)
822 {
823 
824     Q_ASSERT(!key.isNull());
825 
826     const QString email = Formatting::prettyEMail(key);
827     const QString name = Formatting::prettyName(key);
828 
829     if (name.isEmpty()) {
830         return email;
831     } else if (email.isEmpty()) {
832         return name;
833     } else {
834         return QStringLiteral("%1 <%2>").arg(name, email);
835     }
836 }
837 
838 }
839 
summaryToString(const Signature::Summary summary)840 const char *Formatting::summaryToString(const Signature::Summary summary)
841 {
842     if (summary & Signature::Red) {
843         return "RED";
844     }
845     if (summary & Signature::Green) {
846         return "GREEN";
847     }
848     return "YELLOW";
849 }
850 
signatureToString(const Signature & sig,const Key & key)851 QString Formatting::signatureToString(const Signature &sig, const Key &key)
852 {
853     if (sig.isNull()) {
854         return QString();
855     }
856 
857     const bool red   = (sig.summary() & Signature::Red);
858     const bool valid = (sig.summary() & Signature::Valid);
859 
860     if (red) {
861         if (key.isNull()) {
862             if (const char *fpr = sig.fingerprint()) {
863                 return i18n("Bad signature by unknown certificate %1: %2", QString::fromLatin1(fpr), QString::fromLocal8Bit(sig.status().asString()));
864             } else {
865                 return i18n("Bad signature by an unknown certificate: %1", QString::fromLocal8Bit(sig.status().asString()));
866             }
867         } else {
868             return i18n("Bad signature by %1: %2", keyToString(key), QString::fromLocal8Bit(sig.status().asString()));
869         }
870 
871     } else if (valid) {
872         if (key.isNull()) {
873             if (const char *fpr = sig.fingerprint()) {
874                 return i18n("Good signature by unknown certificate %1.", QString::fromLatin1(fpr));
875             } else {
876                 return i18n("Good signature by an unknown certificate.");
877             }
878         } else {
879             return i18n("Good signature by %1.", keyToString(key));
880         }
881 
882     } else if (key.isNull()) {
883         if (const char *fpr = sig.fingerprint()) {
884             return i18n("Invalid signature by unknown certificate %1: %2", QString::fromLatin1(fpr), QString::fromLocal8Bit(sig.status().asString()));
885         } else {
886             return i18n("Invalid signature by an unknown certificate: %1", QString::fromLocal8Bit(sig.status().asString()));
887         }
888     } else {
889         return i18n("Invalid signature by %1: %2", keyToString(key), QString::fromLocal8Bit(sig.status().asString()));
890     }
891 }
892 
893 //
894 // ImportResult
895 //
896 
importMetaData(const Import & import,const QStringList & ids)897 QString Formatting::importMetaData(const Import &import, const QStringList &ids)
898 {
899     const QString result = importMetaData(import);
900     if (result.isEmpty()) {
901         return QString();
902     } else {
903         return result + QLatin1Char('\n') +
904                i18n("This certificate was imported from the following sources:") + QLatin1Char('\n') +
905                ids.join(QLatin1Char('\n'));
906     }
907 }
908 
importMetaData(const Import & import)909 QString Formatting::importMetaData(const Import &import)
910 {
911 
912     if (import.isNull()) {
913         return QString();
914     }
915 
916     if (import.error().isCanceled()) {
917         return i18n("The import of this certificate was canceled.");
918     }
919     if (import.error()) {
920         return i18n("An error occurred importing this certificate: %1",
921                     QString::fromLocal8Bit(import.error().asString()));
922     }
923 
924     const unsigned int status = import.status();
925     if (status & Import::NewKey) {
926         return (status & Import::ContainedSecretKey)
927                ? i18n("This certificate was new to your keystore. The secret key is available.")
928                : i18n("This certificate is new to your keystore.");
929     }
930 
931     QStringList results;
932     if (status & Import::NewUserIDs) {
933         results.push_back(i18n("New user-ids were added to this certificate by the import."));
934     }
935     if (status & Import::NewSignatures) {
936         results.push_back(i18n("New signatures were added to this certificate by the import."));
937     }
938     if (status & Import::NewSubkeys) {
939         results.push_back(i18n("New subkeys were added to this certificate by the import."));
940     }
941 
942     return results.empty()
943            ? i18n("The import contained no new data for this certificate. It is unchanged.")
944            : results.join(QLatin1Char('\n'));
945 }
946 
947 //
948 // Overview in CertificateDetailsDialog
949 //
950 
formatOverview(const Key & key)951 QString Formatting::formatOverview(const Key &key)
952 {
953     return toolTip(key, AllOptions);
954 }
955 
usageString(const Subkey & sub)956 QString Formatting::usageString(const Subkey &sub)
957 {
958     QStringList usageStrings;
959     if (sub.canCertify()) {
960         usageStrings << i18n("Certify");
961     }
962     if (sub.canSign()) {
963         usageStrings << i18n("Sign");
964     }
965     if (sub.canEncrypt()) {
966         usageStrings << i18n("Encrypt");
967     }
968     if (sub.canAuthenticate()) {
969         usageStrings << i18n("Authenticate");
970     }
971     return usageStrings.join(QLatin1String(", "));
972 }
973 
summaryLine(const Key & key)974 QString Formatting::summaryLine(const Key &key)
975 {
976     return keyToString(key) + QLatin1Char(' ') +
977            i18nc("(validity, protocol, creation date)",
978                  "(%1, %2, created: %3)",
979 		 Formatting::complianceStringShort(key),
980 		 displayName(key.protocol()),
981                  Formatting::creationDateString(key));
982 }
983 
summaryLine(const KeyGroup & group)984 QString Formatting::summaryLine(const KeyGroup &group)
985 {
986     switch (group.source()) {
987         case KeyGroup::ApplicationConfig:
988         case KeyGroup::GnuPGConfig:
989             return i18ncp("name of group of keys (n key(s), validity)",
990                           "%2 (1 key, %3)", "%2 (%1 keys, %3)",
991                           group.keys().size(), group.name(), Formatting::complianceStringShort(group));
992         case KeyGroup::Tags:
993             return i18ncp("name of group of keys (n key(s), validity, tag)",
994                           "%2 (1 key, %3, tag)", "%2 (%1 keys, %3, tag)",
995                           group.keys().size(), group.name(), Formatting::complianceStringShort(group));
996         default:
997             return i18ncp("name of group of keys (n key(s), validity, group ...)",
998                           "%2 (1 key, %3, unknown origin)", "%2 (%1 keys, %3, unknown origin)",
999                           group.keys().size(), group.name(), Formatting::complianceStringShort(group));
1000     }
1001 }
1002 
1003 namespace
1004 {
iconForValidity(UserID::Validity validity)1005 QIcon iconForValidity(UserID::Validity validity)
1006 {
1007     switch (validity) {
1008         case UserID::Ultimate:
1009         case UserID::Full:
1010         case UserID::Marginal:
1011             return QIcon::fromTheme(QStringLiteral("emblem-success"));
1012         case UserID::Never:
1013             return QIcon::fromTheme(QStringLiteral("emblem-error"));
1014         case UserID::Undefined:
1015         case UserID::Unknown:
1016         default:
1017             return QIcon::fromTheme(QStringLiteral("emblem-information"));
1018     }
1019 }
1020 }
1021 
1022 // Icon for certificate selection indication
iconForUid(const UserID & uid)1023 QIcon Formatting::iconForUid(const UserID &uid)
1024 {
1025     return iconForValidity(uid.validity());
1026 }
1027 
validity(const UserID & uid)1028 QString Formatting::validity(const UserID &uid)
1029 {
1030     switch (uid.validity()) {
1031         case UserID::Ultimate:
1032             return i18n("The certificate is marked as your own.");
1033         case UserID::Full:
1034             return i18n("The certificate belongs to this recipient.");
1035         case UserID::Marginal:
1036             return i18n("The trust model indicates marginally that the certificate belongs to this recipient.");
1037         case UserID::Never:
1038             return i18n("This certificate should not be used.");
1039         case UserID::Undefined:
1040         case UserID::Unknown:
1041         default:
1042             return i18n("There is no indication that this certificate belongs to this recipient.");
1043     }
1044 }
1045 
validity(const KeyGroup & group)1046 QString Formatting::validity(const KeyGroup &group)
1047 {
1048     if (group.isNull()) {
1049         return QString();
1050     }
1051 
1052     const KeyGroup::Keys &keys = group.keys();
1053     if (keys.size() == 0) {
1054         return i18n("This group does not contain any keys.");
1055     }
1056 
1057     return getValidityStatement(keys);
1058 }
1059 
1060 namespace
1061 {
minimalValidityOfNotRevokedUserIDs(const Key & key)1062 UserID::Validity minimalValidityOfNotRevokedUserIDs(const Key &key)
1063 {
1064     std::vector<UserID> userIDs = key.userIDs();
1065     const auto endOfNotRevokedUserIDs = std::remove_if(userIDs.begin(), userIDs.end(), std::mem_fn(&UserID::isRevoked));
1066     const int minValidity = std::accumulate(userIDs.begin(), endOfNotRevokedUserIDs, UserID::Ultimate + 1,
1067                                             [] (int validity, const UserID &userID) {
1068                                                 return std::min(validity, static_cast<int>(userID.validity()));
1069                                             });
1070     return minValidity <= UserID::Ultimate ? static_cast<UserID::Validity>(minValidity) : UserID::Unknown;
1071 }
1072 
1073 template <typename Container>
minimalValidity(const Container & keys)1074 UserID::Validity minimalValidity(const Container& keys)
1075 {
1076     const int minValidity = std::accumulate(keys.cbegin(), keys.cend(), UserID::Ultimate + 1,
1077                                             [] (int validity, const Key &key) {
1078                                                 return std::min<int>(validity, minimalValidityOfNotRevokedUserIDs(key));
1079                                             });
1080     return minValidity <= UserID::Ultimate ? static_cast<UserID::Validity>(minValidity) : UserID::Unknown;
1081 }
1082 }
1083 
validityIcon(const KeyGroup & group)1084 QIcon Formatting::validityIcon(const KeyGroup &group)
1085 {
1086     return iconForValidity(minimalValidity(group.keys()));
1087 }
1088 
uidsHaveFullValidity(const Key & key)1089 bool Formatting::uidsHaveFullValidity(const Key &key)
1090 {
1091     return minimalValidityOfNotRevokedUserIDs(key) >= UserID::Full;
1092 }
1093 
complianceMode()1094 QString Formatting::complianceMode()
1095 {
1096     const auto complianceValue = getCryptoConfigStringValue("gpg", "compliance");
1097     return complianceValue == QLatin1String("gnupg") ? QString() : complianceValue;
1098 }
1099 
isKeyDeVs(const GpgME::Key & key)1100 bool Formatting::isKeyDeVs(const GpgME::Key &key)
1101 {
1102     for (const auto &sub: key.subkeys()) {
1103         if (sub.isExpired() || sub.isRevoked()) {
1104             // Ignore old subkeys
1105             continue;
1106         }
1107         if (!sub.isDeVs()) {
1108             return false;
1109         }
1110     }
1111     return true;
1112 }
1113 
complianceStringForKey(const GpgME::Key & key)1114 QString Formatting::complianceStringForKey(const GpgME::Key &key)
1115 {
1116     // There will likely be more in the future for other institutions
1117     // for now we only have DE-VS
1118     if (Kleo::gnupgIsDeVsCompliant()) {
1119         if (uidsHaveFullValidity(key) && isKeyDeVs(key)) {
1120             return i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
1121                          "May be used for %1 communication.", deVsString());
1122         } else {
1123             return i18nc("VS-NfD-conforming is a German standard for restricted documents. For which special restrictions about algorithms apply. The string describes if a key is compliant to that..",
1124                          "May <b>not</b> be used for %1 communication.", deVsString());
1125         }
1126     }
1127     return QString();
1128 }
1129 
complianceStringShort(const GpgME::Key & key)1130 QString Formatting::complianceStringShort(const GpgME::Key &key)
1131 {
1132     const bool keyValidityChecked = (key.keyListMode() & GpgME::Validate);
1133     if (keyValidityChecked && Formatting::uidsHaveFullValidity(key)) {
1134         if (Kleo::gnupgIsDeVsCompliant() && Formatting::isKeyDeVs(key)) {
1135             return QStringLiteral("★ ") + deVsString(true);
1136         }
1137         return i18nc("As in all user IDs are valid.", "certified");
1138     }
1139     if (key.isExpired()) {
1140         return i18n("expired");
1141     }
1142     if (key.isRevoked()) {
1143         return i18n("revoked");
1144     }
1145     if (key.isDisabled()) {
1146         return i18n("disabled");
1147     }
1148     if (key.isInvalid()) {
1149         return i18n("invalid");
1150     }
1151     if (keyValidityChecked) {
1152         return i18nc("As in not all user IDs are valid.", "not certified");
1153     }
1154 
1155     return i18nc("The validity of the user IDs has not been/could not be checked", "not checked");
1156 }
1157 
complianceStringShort(const KeyGroup & group)1158 QString Formatting::complianceStringShort(const KeyGroup &group)
1159 {
1160     const KeyGroup::Keys &keys = group.keys();
1161 
1162     const bool allKeysFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Formatting::uidsHaveFullValidity);
1163     if (allKeysFullyValid) {
1164         return i18nc("As in all keys are valid.", "all certified");
1165     }
1166 
1167     return i18nc("As in not all keys are valid.", "not all certified");
1168 }
1169 
prettyID(const char * id)1170 QString Formatting::prettyID(const char *id)
1171 {
1172     if (!id) {
1173         return QString();
1174     }
1175     QString ret = QString::fromLatin1(id).toUpper().replace(QRegularExpression(QStringLiteral("(....)")),
1176                                                             QStringLiteral("\\1 ")).trimmed();
1177     // For the standard 10 group fingerprint let us use a double space in the
1178     // middle to increase readability
1179     if (ret.size() == 49) {
1180         ret.insert(24, QLatin1Char(' '));
1181     }
1182     return ret;
1183 }
1184 
origin(int o)1185 QString Formatting::origin(int o)
1186 {
1187     switch (o) {
1188         case Key::OriginKS:
1189             return i18n("Keyserver");
1190         case Key::OriginDane:
1191             return QStringLiteral("DANE");
1192         case Key::OriginWKD:
1193             return QStringLiteral("WKD");
1194         case Key::OriginURL:
1195             return QStringLiteral("URL");
1196         case Key::OriginFile:
1197             return i18n("File import");
1198         case Key::OriginSelf:
1199             return i18n("Generated");
1200         case Key::OriginOther:
1201         case Key::OriginUnknown:
1202         default:
1203           return i18n("Unknown");
1204     }
1205 }
1206 
deVsString(bool compliant)1207 QString Formatting::deVsString(bool compliant)
1208 {
1209     const auto filter = KeyFilterManager::instance()->keyFilterByID(compliant ?
1210             QStringLiteral("de-vs-filter") :
1211             QStringLiteral("not-de-vs-filter"));
1212     if (!filter) {
1213         return compliant ? i18n("VS-NfD compliant") : i18n("Not VS-NfD compliant");
1214     }
1215     return filter->name();
1216 }
1217 
1218 namespace
1219 {
formatTrustScope(const char * trustScope)1220 QString formatTrustScope(const char *trustScope)
1221 {
1222     static const QRegularExpression escapedNonAlphaNum{QStringLiteral(R"(\\([^0-9A-Za-z]))")};
1223 
1224     const auto scopeRegExp = QString::fromUtf8(trustScope);
1225     if (scopeRegExp.startsWith(u"<[^>]+[@.]") && scopeRegExp.endsWith(u">$")) {
1226         // looks like a trust scope regular expression created by gpg
1227         auto domain = scopeRegExp.mid(10, scopeRegExp.size() - 10 - 2);
1228         domain.replace(escapedNonAlphaNum, QStringLiteral(R"(\1)"));
1229         return domain;
1230     }
1231     return scopeRegExp;
1232 }
1233 }
1234 
trustSignatureDomain(const GpgME::UserID::Signature & sig)1235 QString Formatting::trustSignatureDomain(const GpgME::UserID::Signature &sig)
1236 {
1237 #ifdef GPGMEPP_SUPPORTS_TRUST_SIGNATURES
1238     return formatTrustScope(sig.trustScope());
1239 #else
1240     return {};
1241 #endif
1242 }
1243 
trustSignature(const GpgME::UserID::Signature & sig)1244 QString Formatting::trustSignature(const GpgME::UserID::Signature &sig)
1245 {
1246 #ifdef GPGMEPP_SUPPORTS_TRUST_SIGNATURES
1247     switch (sig.trustValue()) {
1248     case TrustSignatureTrust::Partial:
1249         return i18nc("Certifies this key as partially trusted introducer for 'domain name'.",
1250                      "Certifies this key as partially trusted introducer for '%1'.", trustSignatureDomain(sig));
1251     case TrustSignatureTrust::Complete:
1252         return i18nc("Certifies this key as fully trusted introducer for 'domain name'.",
1253                      "Certifies this key as fully trusted introducer for '%1'.", trustSignatureDomain(sig));
1254     default:
1255         return {};
1256     }
1257 #else
1258     return {};
1259 #endif
1260 }
1261