Libkleo

formatting.cpp
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, 2022 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
16#include "algorithm.h"
17#include "compat.h"
18#include "compliance.h"
19#include "cryptoconfig.h"
20#include "gnupg.h"
21#include "keyhelpers.h"
22
23#include <libkleo/dn.h>
24#include <libkleo/keycache.h>
25#include <libkleo/keygroup.h>
26
27#include <libkleo_debug.h>
28
29#include <KEmailAddress>
30#include <KLocalizedString>
31
32#include <QGpgME/CryptoConfig>
33#include <QGpgME/Protocol>
34
35#include <QDateTime>
36#include <QIcon>
37#include <QLocale>
38#include <QRegularExpression>
39#include <QString>
40
41#include <gpgme++/importresult.h>
42#include <gpgme++/key.h>
43
44#include <gpg-error.h>
45
46using namespace GpgME;
47using namespace Kleo;
48
49using namespace Qt::Literals::StringLiterals;
50
51namespace
52{
53QIcon iconForValidityAndCompliance(UserID::Validity validity, bool isCompliant)
54{
55 switch (validity) {
56 case UserID::Ultimate:
57 case UserID::Full:
58 case UserID::Marginal:
59 return isCompliant ? Formatting::successIcon() : Formatting::infoIcon();
60 case UserID::Never:
61 return Formatting::errorIcon();
62 case UserID::Undefined:
63 case UserID::Unknown:
64 default:
65 return Formatting::infoIcon();
66 }
67}
68QIcon iconForValidity(const UserID &userId)
69{
70 const bool keyIsCompliant = !DeVSCompliance::isActive() || //
71 (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(userId.parent()));
72 return iconForValidityAndCompliance(userId.validity(), keyIsCompliant);
73}
74}
75
76QIcon Formatting::IconProvider::icon(const GpgME::Key &key) const
77{
78 return icon(key.userID(0));
79}
80
81QIcon Formatting::IconProvider::icon(const GpgME::UserID &userID) const
82{
83 if (usage.canEncrypt() && !Kleo::canBeUsedForEncryption(userID.parent())) {
84 return Formatting::errorIcon();
85 }
86 if (usage.canSign() && !Kleo::canBeUsedForSigning(userID.parent())) {
87 return Formatting::errorIcon();
88 }
89 if (userID.parent().isBad() || userID.isBad()) {
90 return Formatting::errorIcon();
91 }
92 if (Kleo::isRevokedOrExpired(userID)) {
93 return Formatting::errorIcon();
94 }
95 return iconForValidity(userID);
96}
97
98QIcon Formatting::IconProvider::icon(const KeyGroup &group) const
99{
100 if (usage.canEncrypt() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForEncryption)) {
101 return Formatting::errorIcon();
102 }
103 if (usage.canSign() && !Kleo::all_of(group.keys(), Kleo::canBeUsedForSigning)) {
104 return Formatting::errorIcon();
105 }
106 return validityIcon(group);
107}
108
109QIcon Formatting::successIcon()
110{
111 return QIcon::fromTheme(QStringLiteral("emblem-success"));
112}
113
114QIcon Formatting::infoIcon()
115{
116 return QIcon::fromTheme(QStringLiteral("emblem-information"));
117}
118
119QIcon Formatting::questionIcon()
120{
121 return QIcon::fromTheme(QStringLiteral("emblem-question"));
122}
123
124QIcon Formatting::unavailableIcon()
125{
126 return QIcon::fromTheme(QStringLiteral("emblem-unavailable"));
127}
128
129QIcon Formatting::warningIcon()
130{
131 return QIcon::fromTheme(QStringLiteral("emblem-warning"));
132}
133
134QIcon Formatting::errorIcon()
135{
136 return QIcon::fromTheme(QStringLiteral("emblem-error"));
137}
138
139//
140// Name
141//
142
143QString Formatting::prettyName(int proto, const char *id, const char *name_, const char *comment_)
144{
145 if (proto == GpgME::OpenPGP) {
146 const QString name = QString::fromUtf8(name_);
147 if (name.isEmpty()) {
148 return QString();
149 }
150 const QString comment = QString::fromUtf8(comment_);
151 if (comment.isEmpty()) {
152 return name;
153 }
154 return QStringLiteral("%1 (%2)").arg(name, comment);
155 }
156
157 if (proto == GpgME::CMS) {
158 const DN subject(id);
159 const QString cn = subject[QStringLiteral("CN")].trimmed();
160 if (cn.isEmpty()) {
161 return subject.prettyDN();
162 }
163 return cn;
164 }
165
166 return QString();
167}
168
169QString Formatting::prettyNameAndEMail(int proto, const char *id, const char *name_, const char *email_, const char *comment_)
170{
171 return prettyNameAndEMail(proto, QString::fromUtf8(id), QString::fromUtf8(name_), prettyEMail(email_, id), QString::fromUtf8(comment_));
172}
173
174QString Formatting::prettyNameAndEMail(int proto, const QString &id, const QString &name, const QString &email, const QString &comment)
175{
176 if (proto == GpgME::OpenPGP) {
177 if (name.isEmpty()) {
178 if (email.isEmpty()) {
179 return QString();
180 } else if (comment.isEmpty()) {
181 return QStringLiteral("<%1>").arg(email);
182 } else {
183 return QStringLiteral("(%2) <%1>").arg(email, comment);
184 }
185 }
186 if (email.isEmpty()) {
187 if (comment.isEmpty()) {
188 return name;
189 } else {
190 return QStringLiteral("%1 (%2)").arg(name, comment);
191 }
192 }
193 if (comment.isEmpty()) {
194 return QStringLiteral("%1 <%2>").arg(name, email);
195 } else {
196 return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment);
197 }
198 }
199
200 if (proto == GpgME::CMS) {
201 const DN subject(id);
202 const QString cn = subject[QStringLiteral("CN")].trimmed();
203 if (cn.isEmpty()) {
204 return subject.prettyDN();
205 }
206 return cn;
207 }
208 return QString();
209}
210
211QString Formatting::prettyUserID(const UserID &uid)
212{
213 if (uid.parent().protocol() == GpgME::OpenPGP) {
214 return prettyNameAndEMail(uid);
215 }
216 const QByteArray id = QByteArray(uid.id()).trimmed();
217 if (id.startsWith('<')) {
218 return prettyEMail(uid.email(), uid.id());
219 }
220 if (id.startsWith('(')) {
221 // ### parse uri/dns:
222 return QString::fromUtf8(uid.id());
223 } else {
224 return DN(uid.id()).prettyDN();
225 }
226}
227
228QString Formatting::prettyKeyID(const char *id)
229{
230 if (!id) {
231 return QString();
232 }
233 return QLatin1StringView("0x") + QString::fromLatin1(id).toUpper();
234}
235
236QString Formatting::prettyNameAndEMail(const UserID &uid)
237{
238 return prettyNameAndEMail(uid.parent().protocol(), uid.id(), uid.name(), uid.email(), uid.comment());
239}
240
241QString Formatting::prettyNameAndEMail(const Key &key)
242{
243 return prettyNameAndEMail(key.userID(0));
244}
245
246QString Formatting::prettyName(const Key &key)
247{
248 return prettyName(key.userID(0));
249}
250
251QString Formatting::prettyName(const UserID &uid)
252{
253 return prettyName(uid.parent().protocol(), uid.id(), uid.name(), uid.comment());
254}
255
256QString Formatting::prettyName(const UserID::Signature &sig)
257{
258 return prettyName(GpgME::OpenPGP, sig.signerUserID(), sig.signerName(), sig.signerComment());
259}
260
261//
262// EMail
263//
264
265QString Formatting::prettyEMail(const Key &key)
266{
267 for (unsigned int i = 0, end = key.numUserIDs(); i < end; ++i) {
268 const QString email = prettyEMail(key.userID(i));
269 if (!email.isEmpty()) {
270 return email;
271 }
272 }
273 return QString();
274}
275
276QString Formatting::prettyEMail(const UserID &uid)
277{
278 return prettyEMail(uid.email(), uid.id());
279}
280
281QString Formatting::prettyEMail(const UserID::Signature &sig)
282{
283 return prettyEMail(sig.signerEmail(), sig.signerUserID());
284}
285
286QString Formatting::prettyEMail(const char *email_, const char *id)
287{
288 QString email;
289 QString name;
290 QString comment;
291 if (email_ && KEmailAddress::splitAddress(QString::fromUtf8(email_), name, email, comment) == KEmailAddress::AddressOk) {
292 return email;
293 } else {
294 return DN(id)[QStringLiteral("EMAIL")].trimmed();
295 }
296}
297
298//
299// Tooltip
300//
301
302namespace
303{
304
305static QString protect_whitespace(QString s)
306{
307 static const QLatin1Char SP(' ');
308 static const QLatin1Char NBSP('\xA0');
309 return s.replace(SP, NBSP);
310}
311
312template<typename T_arg>
313QString format_row(const QString &field, const T_arg &arg)
314{
315 return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").arg(protect_whitespace(field), arg);
316}
317QString format_row(const QString &field, const QString &arg)
318{
319 return QStringLiteral("<tr><th>%1:</th><td>%2</td></tr>").arg(protect_whitespace(field), arg.toHtmlEscaped());
320}
321QString format_row(const QString &field, const char *arg)
322{
323 return format_row(field, QString::fromUtf8(arg));
324}
325
326QString format_keytype(const Key &key)
327{
328 const Subkey subkey = key.subkey(0);
329 if (key.hasSecret()) {
330 return i18n("%1-bit %2 (secret key available)", subkey.length(), QLatin1StringView(subkey.publicKeyAlgorithmAsString()));
331 } else {
332 return i18n("%1-bit %2", subkey.length(), QLatin1StringView(subkey.publicKeyAlgorithmAsString()));
333 }
334}
335
336QString format_subkeytype(const Subkey &subkey)
337{
338 const auto algo = subkey.publicKeyAlgorithm();
339
340 if (algo == Subkey::AlgoECC || algo == Subkey::AlgoECDSA || algo == Subkey::AlgoECDH || algo == Subkey::AlgoEDDSA) {
341 return QString::fromStdString(subkey.algoName());
342 }
343 return i18n("%1-bit %2", subkey.length(), QLatin1StringView(subkey.publicKeyAlgorithmAsString()));
344}
345
346QString format_keyusage(const Key &key)
347{
349 if (Kleo::keyHasSign(key)) {
350 if (key.isQualified()) {
351 capabilities.push_back(i18n("Signing (Qualified)"));
352 } else {
353 capabilities.push_back(i18n("Signing"));
354 }
355 }
356 if (Kleo::keyHasEncrypt(key)) {
357 capabilities.push_back(i18n("Encryption"));
358 }
359 if (Kleo::keyHasCertify(key)) {
360 capabilities.push_back(i18n("Certifying User IDs"));
361 }
362 if (Kleo::keyHasAuthenticate(key)) {
363 capabilities.push_back(i18n("SSH Authentication"));
364 }
365 return capabilities.join(QLatin1StringView(", "));
366}
367
368QString format_subkeyusage(const Subkey &subkey)
369{
371 if (subkey.canSign()) {
372 if (subkey.isQualified()) {
373 capabilities.push_back(i18n("Signing (Qualified)"));
374 } else {
375 capabilities.push_back(i18n("Signing"));
376 }
377 }
378 if (subkey.canEncrypt()) {
379 capabilities.push_back(i18n("Encryption"));
380 }
381 if (subkey.canCertify()) {
382 capabilities.push_back(i18n("Certifying User IDs"));
383 }
384 if (subkey.canAuthenticate()) {
385 capabilities.push_back(i18n("SSH Authentication"));
386 }
387 return capabilities.join(QLatin1StringView(", "));
388}
389
390static QString time_t2string(time_t t)
391{
392 const QDateTime dt = QDateTime::fromSecsSinceEpoch(quint32(t));
394}
395
396static QString make_red(const QString &txt)
397{
398 return QLatin1StringView("<font color=\"red\">") + txt.toHtmlEscaped() + QLatin1StringView("</font>");
399}
400
401}
402
403static QString toolTipInternal(const GpgME::Key &key, const GpgME::UserID &userID, int flags)
404{
405 if (flags == 0 || (key.protocol() != GpgME::CMS && key.protocol() != GpgME::OpenPGP)) {
406 return QString();
407 }
408
409 const Subkey subkey = key.subkey(0);
410
411 QString result;
412 if (flags & Formatting::Validity) {
413 if (key.protocol() == GpgME::OpenPGP || (key.keyListMode() & Validate)) {
414 if (key.isDisabled()) {
415 result = i18n("Disabled");
416 } else if (userID.isRevoked() || key.isRevoked()) {
417 result = make_red(i18n("Revoked"));
418 } else if (key.isExpired()) {
419 result = make_red(i18n("Expired"));
420 } else if (key.keyListMode() & GpgME::Validate) {
421 if (!userID.isNull()) {
422 if (userID.validity() >= UserID::Validity::Full) {
423 result = i18n("User ID is certified.");
424 const auto compliance = Formatting::complianceStringForUserID(userID);
425 if (!compliance.isEmpty()) {
426 result += QStringLiteral("<br>") + compliance;
427 }
428 } else {
429 result = i18n("User ID is not certified.");
430 }
431 } else {
432 unsigned int fullyTrusted = 0;
433 for (const auto &uid : key.userIDs()) {
434 if (uid.validity() >= UserID::Validity::Full) {
435 fullyTrusted++;
436 }
437 }
438 if (fullyTrusted == key.numUserIDs()) {
439 result = i18n("All User IDs are certified.");
440 const auto compliance = Formatting::complianceStringForKey(key);
441 if (!compliance.isEmpty()) {
442 result += QStringLiteral("<br>") + compliance;
443 }
444 } else {
445 result = i18np("One User ID is not certified.", "%1 User IDs are not certified.", key.numUserIDs() - fullyTrusted);
446 }
447 }
448 } else {
449 result = i18n("The validity cannot be checked at the moment.");
450 }
451 } else {
452 result = i18n("The validity cannot be checked at the moment.");
453 }
454 }
455 if (flags == Formatting::Validity) {
456 return result;
457 }
458
459 result += QLatin1StringView("<table border=\"0\">");
460 if (key.protocol() == GpgME::CMS) {
461 if (flags & Formatting::SerialNumber) {
462 result += format_row(i18n("Serial number"), key.issuerSerial());
463 }
464 if (flags & Formatting::Issuer) {
465 result += format_row(i18n("Issuer"), key.issuerName());
466 }
467 }
468 if (flags & Formatting::UserIDs) {
469 if (userID.isNull()) {
470 const std::vector<UserID> uids = key.userIDs();
471 if (!uids.empty()) {
472 result += format_row(key.protocol() == GpgME::CMS ? i18n("Subject") : i18n("User ID"), Formatting::prettyUserID(uids.front()));
473 }
474 if (uids.size() > 1) {
475 for (auto it = uids.begin() + 1, end = uids.end(); it != end; ++it) {
476 if (!it->isRevoked() && !it->isInvalid()) {
477 result += format_row(i18n("a.k.a."), Formatting::prettyUserID(*it));
478 }
479 }
480 }
481 } else {
482 result += format_row(key.protocol() == GpgME::CMS ? i18n("Subject") : i18n("User ID"), Formatting::prettyUserID(userID));
483 }
484 }
485 if (flags & Formatting::ExpiryDates) {
486 result += format_row(i18n("Valid from"), time_t2string(subkey.creationTime()));
487
488 if (!subkey.neverExpires()) {
489 result += format_row(i18n("Valid until"), time_t2string(subkey.expirationTime()));
490 }
491 }
492
493 if (flags & Formatting::CertificateType) {
494 result += format_row(i18n("Type"), format_keytype(key));
495 }
496 if (flags & Formatting::CertificateUsage) {
497 result += format_row(i18n("Usage"), format_keyusage(key));
498 }
499 if (flags & Formatting::KeyID) {
500 result += format_row(i18n("Key ID"), QString::fromLatin1(key.keyID()));
501 }
502 if (flags & Formatting::Fingerprint) {
503 result += format_row(i18n("Fingerprint"), key.primaryFingerprint());
504 }
505 if (flags & Formatting::OwnerTrust) {
506 if (key.protocol() == GpgME::OpenPGP) {
507 result += format_row(i18n("Certification trust"), Formatting::ownerTrustShort(key));
508 } else if (key.isRoot()) {
509 result += format_row(i18n("Trusted issuer?"), (userID.isNull() ? key.userID(0) : userID).validity() == UserID::Ultimate ? i18n("Yes") : i18n("No"));
510 }
511 }
512 if (flags & Formatting::StorageLocation) {
513 if (const char *card = subkey.cardSerialNumber()) {
514 result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card)));
515 } else {
516 result += format_row(i18n("Stored"), i18nc("stored...", "on this computer"));
517 }
518 }
519 if (flags & Formatting::Subkeys) {
520 for (const auto &sub : key.subkeys()) {
521 result += QLatin1StringView("<hr/>");
522 result += format_row(i18n("Subkey"), sub.fingerprint());
523 if (sub.isRevoked()) {
524 result += format_row(i18n("Status"), i18n("Revoked"));
525 } else if (sub.isExpired()) {
526 result += format_row(i18n("Status"), i18n("Expired"));
527 }
528 if (flags & Formatting::ExpiryDates) {
529 result += format_row(i18n("Valid from"), time_t2string(sub.creationTime()));
530
531 if (!sub.neverExpires()) {
532 result += format_row(i18n("Valid until"), time_t2string(sub.expirationTime()));
533 }
534 }
535
536 if (flags & Formatting::CertificateType) {
537 result += format_row(i18n("Type"), format_subkeytype(sub));
538 }
539 if (flags & Formatting::CertificateUsage) {
540 result += format_row(i18n("Usage"), format_subkeyusage(sub));
541 }
542 if (flags & Formatting::StorageLocation) {
543 if (const char *card = sub.cardSerialNumber()) {
544 result += format_row(i18n("Stored"), i18nc("stored...", "on SmartCard with serial no. %1", QString::fromUtf8(card)));
545 } else {
546 result += format_row(i18n("Stored"), i18nc("stored...", "on this computer"));
547 }
548 }
549 }
550 }
551 result += QLatin1StringView("</table>");
552
553 return result;
554}
555
556QString Formatting::toolTip(const Key &key, int flags)
557{
558 return toolTipInternal(key, UserID(), flags);
559}
560
561namespace
562{
563template<typename Container>
564QString getValidityStatement(const Container &keys)
565{
566 const bool allKeysAreOpenPGP = std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) {
567 return key.protocol() == GpgME::OpenPGP;
568 });
569 const bool allKeysAreValidated = std::all_of(keys.cbegin(), keys.cend(), [](const Key &key) {
570 return key.keyListMode() & Validate;
571 });
572 if (allKeysAreOpenPGP || allKeysAreValidated) {
573 const bool someKeysAreBad = std::any_of(keys.cbegin(), keys.cend(), std::mem_fn(&Key::isBad));
574 if (someKeysAreBad) {
575 return i18n("Some keys are revoked, expired, disabled, or invalid.");
576 } else {
577 const bool allKeysAreFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Kleo::allUserIDsHaveFullValidity);
578 if (allKeysAreFullyValid) {
579 return i18n("All keys are certified.");
580 } else {
581 return i18n("Some keys are not certified.");
582 }
583 }
584 }
585 return i18n("The validity of the keys cannot be checked at the moment.");
586}
587}
588
589QString Formatting::toolTip(const KeyGroup &group, int flags)
590{
591 static const unsigned int maxNumKeysForTooltip = 20;
592
593 if (group.isNull()) {
594 return QString();
595 }
596
597 const KeyGroup::Keys &keys = group.keys();
598 if (keys.size() == 0) {
599 return i18nc("@info:tooltip", "This group does not contain any keys.");
600 }
601
602 if (Kleo::any_of(keys, [](const auto &key) {
603 return !key.hasEncrypt();
604 })) {
605 return i18nc("@info:tooltip", "Some of the certificates in this group cannot be used for encryption. Using this group can lead to unexpected results.");
606 }
607
608 const QString validity = (flags & Validity) ? getValidityStatement(keys) : QString();
609 if (flags == Validity) {
610 return validity;
611 }
612
613 // list either up to maxNumKeysForTooltip keys or (maxNumKeysForTooltip-1) keys followed by "and n more keys"
614 const unsigned int numKeysForTooltip = keys.size() > maxNumKeysForTooltip ? maxNumKeysForTooltip - 1 : keys.size();
615
616 QStringList result;
617 result.reserve(3 + 2 + numKeysForTooltip + 2);
618 if (!validity.isEmpty()) {
619 result.push_back(QStringLiteral("<p>"));
620 result.push_back(validity.toHtmlEscaped());
621 result.push_back(QStringLiteral("</p>"));
622 }
623
624 result.push_back(QStringLiteral("<p>"));
625 result.push_back(i18n("Keys:"));
626 {
627 auto it = keys.cbegin();
628 for (unsigned int i = 0; i < numKeysForTooltip; ++i, ++it) {
629 result.push_back(QLatin1StringView("<br>") + Formatting::summaryLine(*it).toHtmlEscaped());
630 }
631 }
632 if (keys.size() > numKeysForTooltip) {
633 result.push_back(QLatin1StringView("<br>")
634 + i18ncp("this follows a list of keys", "and 1 more key", "and %1 more keys", keys.size() - numKeysForTooltip));
635 }
636 result.push_back(QStringLiteral("</p>"));
637
638 return result.join(QLatin1Char('\n'));
639}
640
641QString Formatting::toolTip(const UserID &userID, int flags)
642{
643 return toolTipInternal(userID.parent(), userID, flags);
644}
645
646//
647// Creation and Expiration
648//
649
650namespace
651{
652static QDate time_t2date(time_t t)
653{
654 if (!t) {
655 return {};
656 }
657 const QDateTime dt = QDateTime::fromSecsSinceEpoch(quint32(t));
658 return dt.date();
659}
660static QString accessible_date_format()
661{
662 return i18nc(
663 "date format suitable for screen readers; "
664 "d: day as a number without a leading zero, "
665 "MMMM: localized month name, "
666 "yyyy: year as a four digit number",
667 "MMMM d, yyyy");
668}
669
670template<typename T>
671QString expiration_date_string(const T &tee, const QString &noExpiration)
672{
673 return tee.neverExpires() ? noExpiration : Formatting::dateString(time_t2date(tee.expirationTime()));
674}
675template<typename T>
676QDate creation_date(const T &tee)
677{
678 return time_t2date(tee.creationTime());
679}
680template<typename T>
681QDate expiration_date(const T &tee)
682{
683 return time_t2date(tee.expirationTime());
684}
685}
686
687QString Formatting::dateString(time_t t)
688{
689 return dateString(time_t2date(t));
690}
691
692QString Formatting::dateString(const QDate &date)
693{
694 return QLocale().toString(date, QLocale::ShortFormat);
695}
696
697QString Formatting::accessibleDate(time_t t)
698{
699 return accessibleDate(time_t2date(t));
700}
701
702QString Formatting::accessibleDate(const QDate &date)
703{
704 return QLocale().toString(date, accessible_date_format());
705}
706
707QString Formatting::expirationDateString(const Key &key, const QString &noExpiration)
708{
709 // if key is remote but has a non-zero expiration date (e.g. a key looked up via WKD),
710 // then we assume that the date is valid; if the date is zero for a remote key, then
711 // we don't know if it's unknown or unlimited
712 return isRemoteKey(key) && (key.subkey(0).expirationTime() == 0) //
713 ? i18nc("@info the expiration date of the key is unknown", "unknown")
714 : expiration_date_string(key.subkey(0), noExpiration);
715}
716
717QString Formatting::expirationDateString(const Subkey &subkey, const QString &noExpiration)
718{
719 return expiration_date_string(subkey, noExpiration);
720}
721
722QString Formatting::expirationDateString(const UserID::Signature &sig, const QString &noExpiration)
723{
724 return expiration_date_string(sig, noExpiration);
725}
726
727QDate Formatting::expirationDate(const Key &key)
728{
729 return expiration_date(key.subkey(0));
730}
731
732QDate Formatting::expirationDate(const Subkey &subkey)
733{
734 return expiration_date(subkey);
735}
736
737QDate Formatting::expirationDate(const UserID::Signature &sig)
738{
739 return expiration_date(sig);
740}
741
742QString Formatting::accessibleExpirationDate(const Key &key, const QString &noExpiration)
743{
744 // if key is remote but has a non-zero expiration date (e.g. a key looked up via WKD),
745 // then we assume that the date is valid; if the date is zero for a remote key, then
746 // we don't know if it's unknown or unlimited
747 return isRemoteKey(key) && (key.subkey(0).expirationTime() == 0) //
748 ? i18nc("@info the expiration date of the key is unknown", "unknown")
749 : accessibleExpirationDate(key.subkey(0), noExpiration);
750}
751
752QString Formatting::accessibleExpirationDate(const Subkey &subkey, const QString &noExpiration)
753{
754 if (subkey.neverExpires()) {
755 return noExpiration.isEmpty() ? i18n("unlimited") : noExpiration;
756 } else {
757 return accessibleDate(expirationDate(subkey));
758 }
759}
760
761QString Formatting::accessibleExpirationDate(const UserID::Signature &sig, const QString &noExpiration)
762{
763 if (sig.neverExpires()) {
764 return noExpiration.isEmpty() ? i18n("unlimited") : noExpiration;
765 } else {
766 return accessibleDate(expirationDate(sig));
767 }
768}
769
770QString Formatting::creationDateString(const Key &key)
771{
772 return dateString(creation_date(key.subkey(0)));
773}
774
775QString Formatting::creationDateString(const Subkey &subkey)
776{
777 return dateString(creation_date(subkey));
778}
779
780QString Formatting::creationDateString(const UserID::Signature &sig)
781{
782 return dateString(creation_date(sig));
783}
784
785QDate Formatting::creationDate(const Key &key)
786{
787 return creation_date(key.subkey(0));
788}
789
790QDate Formatting::creationDate(const Subkey &subkey)
791{
792 return creation_date(subkey);
793}
794
795QDate Formatting::creationDate(const UserID::Signature &sig)
796{
797 return creation_date(sig);
798}
799
800QString Formatting::accessibleCreationDate(const Key &key)
801{
802 return accessibleDate(creationDate(key));
803}
804
805QString Formatting::accessibleCreationDate(const Subkey &subkey)
806{
807 return accessibleDate(creationDate(subkey));
808}
809
810//
811// Types
812//
813
814QString Formatting::displayName(GpgME::Protocol p)
815{
816 if (p == GpgME::CMS) {
817 return i18nc("X.509/CMS encryption standard", "S/MIME");
818 }
819 if (p == GpgME::OpenPGP) {
820 return i18n("OpenPGP");
821 }
822 return i18nc("Unknown encryption protocol", "Unknown");
823}
824
825QString Formatting::type(const Key &key)
826{
827 return displayName(key.protocol());
828}
829
830QString Formatting::type(const Subkey &subkey)
831{
832 return QString::fromUtf8(subkey.publicKeyAlgorithmAsString());
833}
834
835QString Formatting::type(const KeyGroup &group)
836{
837 Q_UNUSED(group)
838 return i18nc("a group of keys/certificates", "Group");
839}
840
841//
842// Status / Validity
843//
844
845QString Formatting::ownerTrustShort(const Key &key)
846{
847 return ownerTrustShort(key.ownerTrust());
848}
849
850QString Formatting::ownerTrustShort(Key::OwnerTrust trust)
851{
852 switch (trust) {
853 case Key::Unknown:
854 return i18nc("unknown trust level", "unknown");
855 case Key::Never:
856 return i18n("untrusted");
857 case Key::Marginal:
858 return i18nc("marginal trust", "marginal");
859 case Key::Full:
860 return i18nc("full trust", "full");
861 case Key::Ultimate:
862 return i18nc("ultimate trust", "ultimate");
863 case Key::Undefined:
864 return i18nc("undefined trust", "undefined");
865 default:
866 Q_ASSERT(!"unexpected owner trust value");
867 break;
868 }
869 return QString();
870}
871
872QString Formatting::validityShort(const Subkey &subkey)
873{
874 if (subkey.isDisabled()) {
875 return i18n("disabled");
876 }
877 if (subkey.isRevoked()) {
878 return i18n("revoked");
879 }
880 if (subkey.isExpired()) {
881 return i18n("expired");
882 }
883 if (subkey.isInvalid()) {
884 return i18n("invalid");
885 }
886 return i18nc("as in 'this subkey is ok'", "OK");
887}
888
889QString Formatting::validityShort(const UserID &uid)
890{
891 if (uid.isRevoked()) {
892 return i18n("revoked");
893 }
894 if (uid.isInvalid()) {
895 return i18n("invalid");
896 }
897 switch (uid.validity()) {
898 case UserID::Unknown:
899 return i18nc("unknown trust level", "unknown");
900 case UserID::Undefined:
901 return i18nc("undefined trust", "undefined");
902 case UserID::Never:
903 return i18n("untrusted");
904 case UserID::Marginal:
905 return i18nc("marginal trust", "marginal");
906 case UserID::Full:
907 return i18nc("full trust", "full");
908 case UserID::Ultimate:
909 return i18nc("ultimate trust", "ultimate");
910 }
911 return QString();
912}
913
914QString Formatting::validityShort(const UserID::Signature &sig)
915{
916 switch (sig.status()) {
917 case UserID::Signature::NoError:
918 if (!sig.isInvalid()) {
919 /* See RFC 4880 Section 5.2.1 */
920 switch (sig.certClass()) {
921 case 0x10: /* Generic */
922 case 0x11: /* Persona */
923 case 0x12: /* Casual */
924 case 0x13: /* Positive */
925 return i18n("valid");
926 case 0x30:
927 return i18n("revoked");
928 default:
929 return i18n("class %1", sig.certClass());
930 }
931 }
932 [[fallthrough]];
933 // fall through:
934 case UserID::Signature::GeneralError:
935 return i18n("invalid");
936 case UserID::Signature::SigExpired:
937 return i18n("expired");
938 case UserID::Signature::KeyExpired:
939 return i18n("certificate expired");
940 case UserID::Signature::BadSignature:
941 return i18nc("fake/invalid signature", "bad");
942 case UserID::Signature::NoPublicKey: {
943 /* GnuPG returns the same error for no public key as for expired
944 * or revoked certificates. */
945 const auto key = KeyCache::instance()->findByKeyIDOrFingerprint(sig.signerKeyID());
946 if (key.isNull()) {
947 return i18n("no public key");
948 } else if (key.isDisabled()) {
949 return i18n("key disabled");
950 } else if (key.isRevoked()) {
951 return i18n("key revoked");
952 } else if (key.isExpired()) {
953 return i18n("key expired");
954 }
955 /* can't happen */
956 return QStringLiteral("unknown");
957 }
958 }
959 return QString();
960}
961
962QIcon Formatting::validityIcon(const UserID::Signature &sig)
963{
964 switch (sig.status()) {
965 case UserID::Signature::NoError:
966 if (!sig.isInvalid()) {
967 /* See RFC 4880 Section 5.2.1 */
968 switch (sig.certClass()) {
969 case 0x10: /* Generic */
970 case 0x11: /* Persona */
971 case 0x12: /* Casual */
972 case 0x13: /* Positive */
973 return Formatting::successIcon();
974 case 0x30:
975 return Formatting::errorIcon();
976 default:
977 return QIcon();
978 }
979 }
980 [[fallthrough]];
981 // fall through:
982 case UserID::Signature::BadSignature:
983 case UserID::Signature::GeneralError:
984 return Formatting::errorIcon();
985 case UserID::Signature::SigExpired:
986 case UserID::Signature::KeyExpired:
987 return Formatting::infoIcon();
988 case UserID::Signature::NoPublicKey:
989 return Formatting::questionIcon();
990 }
991 return QIcon();
992}
993
994QString Formatting::formatKeyLink(const Key &key)
995{
996 if (key.isNull()) {
997 return QString();
998 }
999 return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(QLatin1StringView(key.primaryFingerprint()), Formatting::prettyName(key));
1000}
1001
1002QString Formatting::formatForComboBox(const GpgME::Key &key)
1003{
1004 const QString name = prettyName(key);
1005 QString mail = prettyEMail(key);
1006 if (!mail.isEmpty()) {
1007 mail = QLatin1Char('<') + mail + QLatin1Char('>');
1008 }
1009 return i18nc("name, email, key id", "%1 %2 (%3)", name, mail, QLatin1StringView(key.keyID())).simplified();
1010}
1011
1012QString Formatting::nameAndEmailForSummaryLine(const UserID &id)
1013{
1014 Q_ASSERT(!id.isNull());
1015
1016 const QString email = Formatting::prettyEMail(id);
1017 const QString name = Formatting::prettyName(id);
1018
1019 if (name.isEmpty()) {
1020 return email;
1021 } else if (email.isEmpty()) {
1022 return name;
1023 } else {
1024 return QStringLiteral("%1 <%2>").arg(name, email);
1025 }
1026}
1027
1028QString Formatting::nameAndEmailForSummaryLine(const Key &key)
1029{
1030 Q_ASSERT(!key.isNull());
1031
1032 const QString email = Formatting::prettyEMail(key);
1033 const QString name = Formatting::prettyName(key);
1034
1035 if (name.isEmpty()) {
1036 return email;
1037 } else if (email.isEmpty()) {
1038 return name;
1039 } else {
1040 return QStringLiteral("%1 <%2>").arg(name, email);
1041 }
1042}
1043
1044const char *Formatting::summaryToString(const Signature::Summary summary)
1045{
1046 if (summary & Signature::Red) {
1047 return "RED";
1048 }
1049 if (summary & Signature::Green) {
1050 return "GREEN";
1051 }
1052 return "YELLOW";
1053}
1054
1055QString Formatting::signatureToString(const Signature &sig, const Key &key)
1056{
1057 if (sig.isNull()) {
1058 return QString();
1059 }
1060
1061 const bool red = (sig.summary() & Signature::Red);
1062 const bool valid = (sig.summary() & Signature::Valid);
1063
1064 if (red) {
1065 if (key.isNull()) {
1066 if (const char *fpr = sig.fingerprint()) {
1067 return i18n("Bad signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status()));
1068 } else {
1069 return i18n("Bad signature by an unknown certificate: %1", Formatting::errorAsString(sig.status()));
1070 }
1071 } else {
1072 return i18n("Bad signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status()));
1073 }
1074
1075 } else if (valid) {
1076 if (key.isNull()) {
1077 if (const char *fpr = sig.fingerprint()) {
1078 return i18n("Good signature by unknown certificate %1.", QString::fromLatin1(fpr));
1079 } else {
1080 return i18n("Good signature by an unknown certificate.");
1081 }
1082 } else {
1083 return i18n("Good signature by %1.", nameAndEmailForSummaryLine(key));
1084 }
1085
1086 } else if (key.isNull()) {
1087 if (const char *fpr = sig.fingerprint()) {
1088 return i18n("Invalid signature by unknown certificate %1: %2", QString::fromLatin1(fpr), Formatting::errorAsString(sig.status()));
1089 } else {
1090 return i18n("Invalid signature by an unknown certificate: %1", Formatting::errorAsString(sig.status()));
1091 }
1092 } else {
1093 return i18n("Invalid signature by %1: %2", nameAndEmailForSummaryLine(key), Formatting::errorAsString(sig.status()));
1094 }
1095}
1096
1097//
1098// ImportResult
1099//
1100
1101QString Formatting::importMetaData(const Import &import, const QStringList &ids)
1102{
1103 const QString result = importMetaData(import);
1104 if (result.isEmpty()) {
1105 return QString();
1106 } else {
1107 return result + QLatin1Char('\n') + i18n("This certificate was imported from the following sources:") + QLatin1Char('\n') + ids.join(QLatin1Char('\n'));
1108 }
1109}
1110
1111QString Formatting::importMetaData(const Import &import)
1112{
1113 if (import.isNull()) {
1114 return QString();
1115 }
1116
1117 if (import.error().isCanceled()) {
1118 return i18n("The import of this certificate was canceled.");
1119 }
1120 if (import.error()) {
1121 return i18n("An error occurred importing this certificate: %1", Formatting::errorAsString(import.error()));
1122 }
1123
1124 const unsigned int status = import.status();
1125 if (status & Import::NewKey) {
1126 return (status & Import::ContainedSecretKey) ? i18n("This certificate was new to your keystore. The secret key is available.")
1127 : i18n("This certificate is new to your keystore.");
1128 }
1129
1130 QStringList results;
1131 if (status & Import::NewUserIDs) {
1132 results.push_back(i18n("New user-ids were added to this certificate by the import."));
1133 }
1134 if (status & Import::NewSignatures) {
1135 results.push_back(i18n("New signatures were added to this certificate by the import."));
1136 }
1137 if (status & Import::NewSubkeys) {
1138 results.push_back(i18n("New subkeys were added to this certificate by the import."));
1139 }
1140
1141 return results.empty() ? i18n("The import contained no new data for this certificate. It is unchanged.") : results.join(QLatin1Char('\n'));
1142}
1143
1144//
1145// Overview in CertificateDetailsDialog
1146//
1147
1148QString Formatting::formatOverview(const Key &key)
1149{
1150 return toolTip(key, AllOptions);
1151}
1152
1153QString Formatting::usageString(const Subkey &sub)
1154{
1155 QStringList usageStrings;
1156 if (sub.canCertify()) {
1157 usageStrings << i18n("Certify");
1158 }
1159 if (sub.canSign()) {
1160 usageStrings << i18n("Sign");
1161 }
1162 if (sub.canEncrypt()) {
1163 usageStrings << i18n("Encrypt");
1164 }
1165 if (sub.canAuthenticate()) {
1166 usageStrings << i18n("Authenticate");
1167 }
1168 if (sub.canRenc()) {
1169 usageStrings << i18nc("Means 'Additional Decryption Subkey'; Don't try translating that, though.", "ADSK");
1170 }
1171 return usageStrings.join(QLatin1StringView(", "));
1172}
1173
1174QString Formatting::summaryLine(const UserID &id)
1175{
1176 return i18nc("name <email> (validity, protocol, creation date)",
1177 "%1 (%2, %3, created: %4)",
1178 nameAndEmailForSummaryLine(id),
1179 Formatting::complianceStringShort(id),
1180 displayName(id.parent().protocol()),
1181 Formatting::creationDateString(id.parent()));
1182}
1183
1184QString Formatting::summaryLine(const Key &key)
1185{
1186 return nameAndEmailForSummaryLine(key) + QLatin1Char(' ')
1187 + i18nc("(validity, protocol, creation date)",
1188 "(%1, %2, created: %3)",
1189 Formatting::complianceStringShort(key),
1190 displayName(key.protocol()),
1191 Formatting::creationDateString(key));
1192}
1193
1194QString Formatting::summaryLine(const KeyGroup &group)
1195{
1196 switch (group.source()) {
1197 case KeyGroup::ApplicationConfig:
1198 case KeyGroup::GnuPGConfig:
1199 return i18ncp("name of group of keys (n key(s), validity)",
1200 "%2 (1 key, %3)",
1201 "%2 (%1 keys, %3)",
1202 group.keys().size(),
1203 group.name(),
1204 Formatting::complianceStringShort(group));
1205 case KeyGroup::Tags:
1206 return i18ncp("name of group of keys (n key(s), validity, tag)",
1207 "%2 (1 key, %3, tag)",
1208 "%2 (%1 keys, %3, tag)",
1209 group.keys().size(),
1210 group.name(),
1211 Formatting::complianceStringShort(group));
1212 default:
1213 return i18ncp("name of group of keys (n key(s), validity, group ...)",
1214 "%2 (1 key, %3, unknown origin)",
1215 "%2 (%1 keys, %3, unknown origin)",
1216 group.keys().size(),
1217 group.name(),
1218 Formatting::complianceStringShort(group));
1219 }
1220}
1221
1222// Icon for certificate selection indication
1223QIcon Formatting::iconForUid(const UserID &uid)
1224{
1225 if (Kleo::isRevokedOrExpired(uid)) {
1226 return Formatting::errorIcon();
1227 }
1228 return iconForValidity(uid);
1229}
1230
1231QString Formatting::validity(const UserID &uid)
1232{
1233 switch (uid.validity()) {
1234 case UserID::Ultimate:
1235 return i18n("The certificate is marked as your own.");
1236 case UserID::Full:
1237 return i18n("The certificate belongs to this recipient.");
1238 case UserID::Marginal:
1239 return i18n("The trust model indicates marginally that the certificate belongs to this recipient.");
1240 case UserID::Never:
1241 return i18n("This certificate should not be used.");
1242 case UserID::Undefined:
1243 case UserID::Unknown:
1244 default:
1245 return i18n("There is no indication that this certificate belongs to this recipient.");
1246 }
1247}
1248
1249QString Formatting::validity(const KeyGroup &group)
1250{
1251 if (group.isNull()) {
1252 return QString();
1253 }
1254
1255 const KeyGroup::Keys &keys = group.keys();
1256 if (keys.size() == 0) {
1257 return i18n("This group does not contain any keys.");
1258 }
1259
1260 return getValidityStatement(keys);
1261}
1262
1263namespace
1264{
1265template<typename Container>
1266UserID::Validity minimalValidity(const Container &keys)
1267{
1268 const int minValidity = std::accumulate(keys.cbegin(), keys.cend(), UserID::Ultimate + 1, [](int validity, const Key &key) {
1269 return std::min<int>(validity, minimalValidityOfNotRevokedUserIDs(key));
1270 });
1271 return minValidity <= UserID::Ultimate ? static_cast<UserID::Validity>(minValidity) : UserID::Unknown;
1272}
1273
1274template<typename Container>
1275bool allKeysAreCompliant(const Container &keys)
1276{
1277 if (!DeVSCompliance::isActive()) {
1278 return true;
1279 }
1280 if (!DeVSCompliance::isCompliant()) {
1281 return false;
1282 }
1283 return Kleo::all_of(keys, DeVSCompliance::keyIsCompliant);
1284}
1285}
1286
1287QIcon Formatting::validityIcon(const KeyGroup &group)
1288{
1289 if (Kleo::any_of(group.keys(), std::mem_fn(&Key::isBad))) {
1290 return Formatting::errorIcon();
1291 }
1292 return iconForValidityAndCompliance(minimalValidity(group.keys()), allKeysAreCompliant(group.keys()));
1293}
1294
1295bool Formatting::uidsHaveFullValidity(const Key &key)
1296{
1297 return allUserIDsHaveFullValidity(key);
1298}
1299
1300QString Formatting::complianceMode()
1301{
1302 const auto complianceValue = getCryptoConfigStringValue("gpg", "compliance");
1303 return complianceValue == QLatin1StringView("gnupg") ? QString() : complianceValue;
1304}
1305
1306bool Formatting::isKeyDeVs(const GpgME::Key &key)
1307{
1308 return DeVSCompliance::allSubkeysAreCompliant(key);
1309}
1310
1311QString Formatting::complianceStringForKey(const GpgME::Key &key)
1312{
1313 // There will likely be more in the future for other institutions
1314 // for now we only have DE-VS
1315 if (DeVSCompliance::isCompliant()) {
1316 return isRemoteKey(key) //
1317 ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown")
1318 : DeVSCompliance::name(DeVSCompliance::keyIsCompliant(key));
1319 }
1320 return QString();
1321}
1322
1323QString Formatting::complianceStringForUserID(const GpgME::UserID &userID)
1324{
1325 // There will likely be more in the future for other institutions
1326 // for now we only have DE-VS
1327 if (DeVSCompliance::isCompliant()) {
1328 return isRemoteKey(userID.parent()) //
1329 ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown")
1330 : DeVSCompliance::name(DeVSCompliance::userIDIsCompliant(userID));
1331 }
1332 return QString();
1333}
1334
1335QString Formatting::complianceStringShort(const GpgME::UserID &id)
1336{
1337 if (DeVSCompliance::isCompliant() && DeVSCompliance::userIDIsCompliant(id)) {
1338 return QStringLiteral("★ ") + DeVSCompliance::name(true);
1339 }
1340 const bool keyValidityChecked = (id.parent().keyListMode() & GpgME::Validate);
1341 if (keyValidityChecked && id.validity() >= UserID::Full) {
1342 return i18nc("As in 'this user ID is valid.'", "certified");
1343 }
1344 if (id.parent().isDisabled()) {
1345 return i18n("disabled");
1346 }
1347 if (id.parent().isRevoked() || id.isRevoked()) {
1348 return i18n("revoked");
1349 }
1350 if (id.parent().isExpired() || isExpired(id)) {
1351 return i18n("expired");
1352 }
1353 if (id.parent().isInvalid() || id.isInvalid()) {
1354 return i18n("invalid");
1355 }
1356 if (keyValidityChecked) {
1357 return i18nc("As in 'this user ID is not certified'", "not certified");
1358 }
1359
1360 return i18nc("The validity of this user ID has not been/could not be checked", "not checked");
1361}
1362
1363QString Formatting::complianceStringShort(const GpgME::Key &key)
1364{
1365 if (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(key)) {
1366 return QStringLiteral("★ ") + DeVSCompliance::name(true);
1367 }
1368 const bool keyValidityChecked = (key.keyListMode() & GpgME::Validate);
1369 if (key.isDisabled()) {
1370 return i18n("disabled");
1371 }
1372 if (key.isRevoked()) {
1373 return i18n("revoked");
1374 }
1375 if (key.isExpired()) {
1376 return i18n("expired");
1377 }
1378 if (key.isInvalid()) {
1379 return i18n("invalid");
1380 }
1381 if (keyValidityChecked && Kleo::allUserIDsHaveFullValidity(key)) {
1382 return i18nc("As in all user IDs are valid.", "certified");
1383 }
1384 if (keyValidityChecked) {
1385 return i18nc("As in not all user IDs are valid.", "not certified");
1386 }
1387
1388 return i18nc("The validity of the user IDs has not been/could not be checked", "not checked");
1389}
1390
1391QString Formatting::complianceStringShort(const KeyGroup &group)
1392{
1393 const KeyGroup::Keys &keys = group.keys();
1394
1395 const bool allKeysFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Kleo::allUserIDsHaveFullValidity);
1396 if (allKeysFullyValid) {
1397 return i18nc("As in all keys are valid.", "all certified");
1398 }
1399
1400 return i18nc("As in not all keys are valid.", "not all certified");
1401}
1402
1403QString Formatting::prettyID(const char *id)
1404{
1405 if (!id) {
1406 return QString();
1407 }
1409 if (ret.size() == 64) {
1410 // looks like a V5 fingerprint; format the first 25 bytes as 10 groups of 5 hex characters
1411 ret.truncate(50);
1412 return ret.replace(QRegularExpression(QStringLiteral("(.....)")), QStringLiteral("\\1 ")).trimmed();
1413 }
1414 ret = ret.replace(QRegularExpression(QStringLiteral("(....)")), QStringLiteral("\\1 ")).trimmed();
1415 // For the standard 10 group V4 fingerprint let us use a double space in the
1416 // middle to increase readability
1417 if (ret.size() == 49) {
1418 ret.insert(24, QLatin1Char(' '));
1419 }
1420 return ret;
1421}
1422
1423QString Formatting::accessibleHexID(const char *id)
1424{
1425 static const QRegularExpression groupOfFourRegExp{QStringLiteral("(?:(.)(.)(.)(.))")};
1426 static const QRegularExpression groupOfFiveRegExp{QStringLiteral("(?:(.)(.)(.)(.)(.))")};
1427
1428 QString ret;
1429 ret = QString::fromLatin1(id);
1430 if (ret.size() == 64) {
1431 ret.truncate(50);
1432 return ret.replace(groupOfFiveRegExp, QStringLiteral("\\1 \\2 \\3 \\4 \\5, ")).chopped(2);
1433 }
1434 if (!ret.isEmpty() && (ret.size() % 4 == 0)) {
1435 ret = ret.replace(groupOfFourRegExp, QStringLiteral("\\1 \\2 \\3 \\4, ")).chopped(2);
1436 }
1437 return ret;
1438}
1439
1440QString Formatting::origin(int o)
1441{
1442 switch (o) {
1443 case Key::OriginKS:
1444 return i18n("Keyserver");
1445 case Key::OriginDane:
1446 return QStringLiteral("DANE");
1447 case Key::OriginWKD:
1448 return QStringLiteral("WKD");
1449 case Key::OriginURL:
1450 return QStringLiteral("URL");
1451 case Key::OriginFile:
1452 return i18n("File import");
1453 case Key::OriginSelf:
1454 return i18n("Generated");
1455 case Key::OriginOther:
1456 case Key::OriginUnknown:
1457 default:
1458 return {};
1459 }
1460}
1461
1462QString Formatting::deVsString(bool compliant)
1463{
1464 return DeVSCompliance::name(compliant);
1465}
1466
1467namespace
1468{
1469QString formatTrustScope(const char *trustScope)
1470{
1471 static const QRegularExpression escapedNonAlphaNum{QStringLiteral(R"(\\‍([^0-9A-Za-z]))")};
1472
1473 const auto scopeRegExp = QString::fromUtf8(trustScope);
1474 if (scopeRegExp.startsWith(u"<[^>]+[@.]") && scopeRegExp.endsWith(u">$")) {
1475 // looks like a trust scope regular expression created by gpg
1476 auto domain = scopeRegExp.mid(10, scopeRegExp.size() - 10 - 2);
1477 domain.replace(escapedNonAlphaNum, QStringLiteral(R"(\1)"));
1478 return domain;
1479 }
1480 return scopeRegExp;
1481}
1482}
1483
1484QString Formatting::trustSignatureDomain(const GpgME::UserID::Signature &sig)
1485{
1486 return formatTrustScope(sig.trustScope());
1487}
1488
1489QString Formatting::trustSignature(const GpgME::UserID::Signature &sig)
1490{
1491 switch (sig.trustValue()) {
1492 case TrustSignatureTrust::Partial:
1493 return i18nc("Certifies this key as partially trusted introducer for 'domain name'.",
1494 "Certifies this key as partially trusted introducer for '%1'.",
1495 trustSignatureDomain(sig));
1496 case TrustSignatureTrust::Complete:
1497 return i18nc("Certifies this key as fully trusted introducer for 'domain name'.",
1498 "Certifies this key as fully trusted introducer for '%1'.",
1499 trustSignatureDomain(sig));
1500 default:
1501 return {};
1502 }
1503}
1504
1505QString Formatting::errorAsString(const GpgME::Error &error)
1506{
1507#ifdef Q_OS_WIN
1508 // On Windows, we set GpgME resp. libgpg-error to return (translated) error messages as UTF-8
1509#if GPGMEPP_ERROR_HAS_ASSTDSTRING
1510 const std::string s = error.asStdString();
1511 qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1);
1512 qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s;
1513 qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray::fromStdString(s).toPercentEncoding();
1514 return QString::fromStdString(s);
1515#else
1516 const char *s = error.asString();
1517 qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1);
1518 qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s;
1519 qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray{s}.toPercentEncoding();
1520 return QString::fromUtf8(s);
1521#endif
1522#else
1523#if GPGMEPP_ERROR_HAS_ASSTDSTRING
1524 const std::string s = error.asStdString();
1525 return QString::fromLocal8Bit(QByteArrayView{s.data(), qsizetype(s.size())});
1526#else
1527 return QString::fromLocal8Bit(error.asString());
1528#endif
1529#endif
1530}
1531
1532QString Formatting::prettyAlgorithmName(const std::string &algorithm)
1533{
1534 static const std::map<std::string, QString> displayNames = {
1535 {"brainpoolP256r1", i18nc("@info", "ECC (Brainpool P-256)")},
1536 {"brainpoolP384r1", i18nc("@info", "ECC (Brainpool P-384)")},
1537 {"brainpoolP512r1", i18nc("@info", "ECC (Brainpool P-512)")},
1538 {"curve25519", i18nc("@info", "ECC (Curve25519)")},
1539 {"curve448", i18nc("@info", "ECC (Curve448)")},
1540 {"ed25519", i18nc("@info", "ECC (Ed25519)")},
1541 {"ed448", i18nc("@info", "ECC (Ed448)")},
1542 {"cv25519", i18nc("@info", "ECC (Cv25519)")},
1543 {"cv448", i18nc("@info", "ECC (Cv448)")},
1544 {"nistp256", i18nc("@info", "ECC (NIST P-256)")},
1545 {"nistp384", i18nc("@info", "ECC (NIST P-384)")},
1546 {"nistp521", i18nc("@info", "ECC (NIST P-521)")},
1547 {"rsa2048", i18nc("@info", "RSA 2048")},
1548 {"rsa3072", i18nc("@info", "RSA 3072")},
1549 {"rsa4096", i18nc("@info", "RSA 4096")},
1550 {"dsa1024", i18nc("@info", "DSA 1024")},
1551 {"dsa2048", i18nc("@info", "DSA 2048")},
1552 {"elg1024", i18nc("@info", "Elgamal 1024")},
1553 {"elg2048", i18nc("@info", "Elgamal 2048")},
1554 {"elg3072", i18nc("@info", "Elgamal 3072")},
1555 {"elg4096", i18nc("@info", "Elgamal 4096")},
1556 };
1557 const auto it = displayNames.find(algorithm);
1558 return (it != displayNames.end()) ? it->second : i18nc("@info", "Unknown algorithm");
1559}
1560
1561static QString formatValidSignatureWithTrustLevel(const GpgME::UserID &id)
1562{
1563 if (id.isNull()) {
1564 return QString();
1565 }
1566 switch (id.validity()) {
1567 case GpgME::UserID::Marginal:
1568 return i18n("The signature is valid but the trust in the certificate's validity is only marginal.");
1569 case GpgME::UserID::Full:
1570 return i18n("The signature is valid and the certificate's validity is fully trusted.");
1571 case GpgME::UserID::Ultimate:
1572 return i18n("The signature is valid and the certificate's validity is ultimately trusted.");
1573 case GpgME::UserID::Never:
1574 return i18n("The signature is valid but the certificate's validity is <em>not trusted</em>.");
1575 case GpgME::UserID::Unknown:
1576 return i18n("The signature is valid but the certificate's validity is unknown.");
1577 case GpgME::UserID::Undefined:
1578 default:
1579 return i18n("The signature is valid but the certificate's validity is undefined.");
1580 }
1581}
1582
1583static QString renderKeyLink(const QString &fpr, const QString &text)
1584{
1585 return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(fpr, text.toHtmlEscaped());
1586}
1587
1588static QString renderKey(const GpgME::Key &key)
1589{
1590 if (key.isNull()) {
1591 return i18n("Unknown certificate");
1592 }
1593
1594 return renderKeyLink(QLatin1StringView(key.primaryFingerprint()),
1595 i18nc("User ID (Key ID)", "%1 (%2)", Formatting::prettyNameAndEMail(key), Formatting::prettyID(key.subkey(0).keyID())));
1596}
1597
1598static QString formatSigningInformation(const GpgME::Signature &sig, const GpgME::Key &key)
1599{
1600 if (sig.isNull()) {
1601 return QString();
1602 }
1603 QString text;
1604 const QDateTime dt = sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime();
1605
1606 if (key.isNull()) {
1607 const QString id = u"<br/>"_s + Formatting::prettyID(sig.fingerprint());
1608 if (dt.isValid()) {
1609 return i18nc("1 is a date", "Signature created on %1 using an unknown certificate with fingerprint %2", QLocale().toString(dt), id);
1610 }
1611 return i18n("Signature created using an unknown certificate with fingerprint %1", id);
1612 }
1613
1614 if (dt.isValid()) {
1615 text += i18nc("1 is a date", "Signature created on %1 with certificate: %2", QLocale().toString(dt), renderKey(key));
1616 } else {
1617 text += i18n("Signature created with certificate: %1", renderKey(key));
1618 }
1619
1620 if (Kleo::DeVSCompliance::isCompliant() && ((sig.summary() & GpgME::Signature::Valid) || (sig.summary() & GpgME::Signature::Green))) {
1621 text += (QStringLiteral("<br/>")
1622 + (sig.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
1623 "The signature is %1",
1624 Kleo::DeVSCompliance::name(true))
1625 : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
1626 "The signature <b>is not</b> %1.",
1627 Kleo::DeVSCompliance::name(true))));
1628 }
1629
1630 return text;
1631}
1632
1633static QString signatureSummaryToString(GpgME::Signature::Summary summary)
1634{
1635 if (summary & GpgME::Signature::None) {
1636 return i18n("Error: Signature not verified");
1637 } else if ((summary & GpgME::Signature::Valid) || (summary & GpgME::Signature::Green)) {
1638 return i18n("Good signature");
1639 } else if (summary & GpgME::Signature::KeyRevoked) {
1640 return i18n("Signing certificate was revoked");
1641 } else if (summary & GpgME::Signature::KeyExpired) {
1642 return i18n("Signing certificate is expired");
1643 } else if (summary & GpgME::Signature::KeyMissing) {
1644 return i18n("Certificate is not available");
1645 } else if (summary & GpgME::Signature::SigExpired) {
1646 return i18n("Signature expired");
1647 } else if (summary & GpgME::Signature::CrlMissing) {
1648 return i18n("CRL missing");
1649 } else if (summary & GpgME::Signature::CrlTooOld) {
1650 return i18n("CRL too old");
1651 } else if (summary & GpgME::Signature::BadPolicy) {
1652 return i18n("Bad policy");
1653 } else if (summary & GpgME::Signature::SysError) {
1654 return i18n("System error"); // ### retrieve system error details?
1655 } else if (summary & GpgME::Signature::Red) {
1656 return i18n("Bad signature");
1657 }
1658 return QString();
1659}
1660
1661static QLatin1StringView stripAngleBrackets(const QLatin1StringView &str)
1662{
1663 if (str.isEmpty()) {
1664 return str;
1665 }
1666 if (str[0] == '<' && str[str.size() - 1] == '>') {
1667 return str.mid(1, str.size() - 2);
1668 }
1669 return str;
1670}
1671
1672QString Formatting::email(const GpgME::UserID &uid)
1673{
1674 if (uid.parent().protocol() == GpgME::OpenPGP) {
1675 const QLatin1StringView email(uid.email());
1676 if (!email.isEmpty()) {
1677 return stripAngleBrackets(email).toString();
1678 }
1679 return {};
1680 }
1681
1682 Q_ASSERT(uid.parent().protocol() == GpgME::CMS);
1683
1684 const QLatin1StringView id(uid.id());
1685 if (!id.isEmpty()) {
1686 if (id[0] == '<') {
1687 return stripAngleBrackets(id).toString();
1688 }
1689 return Kleo::DN(id)[QStringLiteral("EMAIL")].trimmed();
1690 }
1691 return {};
1692}
1693
1694static GpgME::UserID findUserIDByMailbox(const GpgME::Key &key, const QString &email)
1695{
1696 const auto userIDs{key.userIDs()};
1697 for (const GpgME::UserID &id : userIDs) {
1698 if (Formatting::email(id).compare(email, Qt::CaseInsensitive)) {
1699 return id;
1700 }
1701 }
1702 return {};
1703}
1704
1705QString Kleo::Formatting::prettySignature(const GpgME::Signature &sig, const QString &sender)
1706{
1707 if (sig.isNull()) {
1708 return QString();
1709 }
1710
1711 const GpgME::Key key = Kleo::KeyCache::instance()->findSigner(sig);
1712
1713 const QString text = formatSigningInformation(sig, key) + QLatin1StringView("<br/>");
1714
1715 // Green
1716 if (sig.summary() & GpgME::Signature::Valid) {
1717 GpgME::UserID id = findUserIDByMailbox(key, sender);
1718 if (id.isNull()) {
1719 for (int i = 0, count = key.userIDs().size(); i < count; i++) {
1720 id = key.userID(i);
1721 if (!id.isNull()) {
1722 break;
1723 }
1724 }
1725 }
1726
1727 return text + formatValidSignatureWithTrustLevel(!id.isNull() ? id : key.userID(0));
1728 }
1729
1730 // Red
1731 if ((sig.summary() & GpgME::Signature::Red)) {
1732 const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
1733 if (sig.summary() & GpgME::Signature::SysError) {
1734 return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status()));
1735 }
1736 return ret;
1737 }
1738
1739 // Key missing
1740 if ((sig.summary() & GpgME::Signature::KeyMissing)) {
1741 return text + i18n("You can search the certificate on a keyserver or import it from a file.");
1742 }
1743
1744 // Yellow
1745 if ((sig.validity() & GpgME::Signature::Validity::Undefined) //
1746 || (sig.validity() & GpgME::Signature::Validity::Unknown) //
1747 || (sig.summary() == GpgME::Signature::Summary::None)) {
1748 return text
1749 + (key.protocol() == GpgME::OpenPGP
1750 ? i18n("The used key is not certified by you or any trusted person.")
1751 : i18n("The used certificate is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown."));
1752 }
1753
1754 // Catch all fall through
1755 const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
1756 if (sig.summary() & GpgME::Signature::SysError) {
1757 return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status()));
1758 }
1759 return ret;
1760}
DN parser and reorderer.
Definition dn.h:27
QString prettyDN() const
Definition dn.cpp:439
Q_SCRIPTABLE CaptureState status()
KCODECS_EXPORT EmailParseResult splitAddress(const QByteArray &address, QByteArray &displayName, QByteArray &addrSpec, QByteArray &comment)
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QString i18ncp(const char *context, const char *singular, const char *plural, const TYPE &arg...)
Capabilities capabilities()
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QAction * mail(const QObject *recvr, const char *slot, QObject *parent)
QByteArray fromStdString(const std::string &str)
QByteArray toPercentEncoding(const QByteArray &exclude, const QByteArray &include, char percent) const const
QByteArray trimmed() const const
QDate date() const const
QDateTime fromSecsSinceEpoch(qint64 secs)
bool isValid() const const
QIcon fromTheme(const QString &name)
bool isEmpty() const const
QLatin1StringView mid(qsizetype start, qsizetype length) const const
qsizetype size() const const
QString toString() const const
bool empty() const const
void push_back(parameter_type value)
void reserve(qsizetype size)
QString toString(QDate date, FormatType format) const const
QString arg(Args &&... args) const const
QString chopped(qsizetype len) const const
QString fromLatin1(QByteArrayView str)
QString fromLocal8Bit(QByteArrayView str)
QString fromStdString(const std::string &str)
QString fromUtf8(QByteArrayView str)
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString simplified() const const
qsizetype size() const const
QString toHtmlEscaped() const const
QString toUpper() const const
QString trimmed() const const
void truncate(qsizetype position)
QString join(QChar separator) const const
CaseInsensitive
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:11:58 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.