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