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
1295QString Formatting::complianceMode()
1296{
1297 const auto complianceValue = getCryptoConfigStringValue("gpg", "compliance");
1298 return complianceValue == QLatin1StringView("gnupg") ? QString() : complianceValue;
1299}
1300
1301QString Formatting::complianceStringForKey(const GpgME::Key &key)
1302{
1303 // There will likely be more in the future for other institutions
1304 // for now we only have DE-VS
1305 if (DeVSCompliance::isCompliant()) {
1306 return isRemoteKey(key) //
1307 ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown")
1308 : DeVSCompliance::name(DeVSCompliance::keyIsCompliant(key));
1309 }
1310 return QString();
1311}
1312
1313QString Formatting::complianceStringForUserID(const GpgME::UserID &userID)
1314{
1315 // There will likely be more in the future for other institutions
1316 // for now we only have DE-VS
1317 if (DeVSCompliance::isCompliant()) {
1318 return isRemoteKey(userID.parent()) //
1319 ? i18nc("@info the compliance of the key with certain requirements is unknown", "unknown")
1320 : DeVSCompliance::name(DeVSCompliance::userIDIsCompliant(userID));
1321 }
1322 return QString();
1323}
1324
1325QString Formatting::complianceStringShort(const GpgME::UserID &id)
1326{
1327 if (DeVSCompliance::isCompliant() && DeVSCompliance::userIDIsCompliant(id)) {
1328 return QStringLiteral("★ ") + DeVSCompliance::name(true);
1329 }
1330 const bool keyValidityChecked = (id.parent().keyListMode() & GpgME::Validate);
1331 if (keyValidityChecked && id.validity() >= UserID::Full) {
1332 return i18nc("As in 'this user ID is valid.'", "certified");
1333 }
1334 if (id.parent().isDisabled()) {
1335 return i18n("disabled");
1336 }
1337 if (id.parent().isRevoked() || id.isRevoked()) {
1338 return i18n("revoked");
1339 }
1340 if (id.parent().isExpired() || isExpired(id)) {
1341 return i18n("expired");
1342 }
1343 if (id.parent().isInvalid() || id.isInvalid()) {
1344 return i18n("invalid");
1345 }
1346 if (keyValidityChecked) {
1347 return i18nc("As in 'this user ID is not certified'", "not certified");
1348 }
1349
1350 return i18nc("The validity of this user ID has not been/could not be checked", "not checked");
1351}
1352
1353QString Formatting::complianceStringShort(const GpgME::Key &key)
1354{
1355 if (DeVSCompliance::isCompliant() && DeVSCompliance::keyIsCompliant(key)) {
1356 return QStringLiteral("★ ") + DeVSCompliance::name(true);
1357 }
1358 const bool keyValidityChecked = (key.keyListMode() & GpgME::Validate);
1359 if (key.isDisabled()) {
1360 return i18n("disabled");
1361 }
1362 if (key.isRevoked()) {
1363 return i18n("revoked");
1364 }
1365 if (key.isExpired()) {
1366 return i18n("expired");
1367 }
1368 if (key.isInvalid()) {
1369 return i18n("invalid");
1370 }
1371 if (keyValidityChecked && Kleo::allUserIDsHaveFullValidity(key)) {
1372 return i18nc("As in all user IDs are valid.", "certified");
1373 }
1374 if (keyValidityChecked) {
1375 return i18nc("As in not all user IDs are valid.", "not certified");
1376 }
1377
1378 return i18nc("The validity of the user IDs has not been/could not be checked", "not checked");
1379}
1380
1381QString Formatting::complianceStringShort(const KeyGroup &group)
1382{
1383 const KeyGroup::Keys &keys = group.keys();
1384
1385 const bool allKeysFullyValid = std::all_of(keys.cbegin(), keys.cend(), &Kleo::allUserIDsHaveFullValidity);
1386 if (allKeysFullyValid) {
1387 return i18nc("As in all keys are valid.", "all certified");
1388 }
1389
1390 return i18nc("As in not all keys are valid.", "not all certified");
1391}
1392
1393QString Formatting::prettyID(const char *id)
1394{
1395 if (!id) {
1396 return QString();
1397 }
1399 if (ret.size() == 64) {
1400 // looks like a V5 fingerprint; format the first 25 bytes as 10 groups of 5 hex characters
1401 ret.truncate(50);
1402 return ret.replace(QRegularExpression(QStringLiteral("(.....)")), QStringLiteral("\\1 ")).trimmed();
1403 }
1404 ret = ret.replace(QRegularExpression(QStringLiteral("(....)")), QStringLiteral("\\1 ")).trimmed();
1405 // For the standard 10 group V4 fingerprint let us use a double space in the
1406 // middle to increase readability
1407 if (ret.size() == 49) {
1408 ret.insert(24, QLatin1Char(' '));
1409 }
1410 return ret;
1411}
1412
1413QString Formatting::accessibleHexID(const char *id)
1414{
1415 static const QRegularExpression groupOfFourRegExp{QStringLiteral("(?:(.)(.)(.)(.))")};
1416 static const QRegularExpression groupOfFiveRegExp{QStringLiteral("(?:(.)(.)(.)(.)(.))")};
1417
1418 QString ret;
1419 ret = QString::fromLatin1(id);
1420 if (ret.size() == 64) {
1421 ret.truncate(50);
1422 return ret.replace(groupOfFiveRegExp, QStringLiteral("\\1 \\2 \\3 \\4 \\5, ")).chopped(2);
1423 }
1424 if (!ret.isEmpty() && (ret.size() % 4 == 0)) {
1425 ret = ret.replace(groupOfFourRegExp, QStringLiteral("\\1 \\2 \\3 \\4, ")).chopped(2);
1426 }
1427 return ret;
1428}
1429
1430QString Formatting::origin(int o)
1431{
1432 switch (o) {
1433 case Key::OriginKS:
1434 return i18n("Keyserver");
1435 case Key::OriginDane:
1436 return QStringLiteral("DANE");
1437 case Key::OriginWKD:
1438 return QStringLiteral("WKD");
1439 case Key::OriginURL:
1440 return QStringLiteral("URL");
1441 case Key::OriginFile:
1442 return i18n("File import");
1443 case Key::OriginSelf:
1444 return i18n("Generated");
1445 case Key::OriginOther:
1446 case Key::OriginUnknown:
1447 default:
1448 return {};
1449 }
1450}
1451
1452QString Formatting::deVsString(bool compliant)
1453{
1454 return DeVSCompliance::name(compliant);
1455}
1456
1457namespace
1458{
1459QString formatTrustScope(const char *trustScope)
1460{
1461 static const QRegularExpression escapedNonAlphaNum{QStringLiteral(R"(\\‍([^0-9A-Za-z]))")};
1462
1463 const auto scopeRegExp = QString::fromUtf8(trustScope);
1464 if (scopeRegExp.startsWith(u"<[^>]+[@.]") && scopeRegExp.endsWith(u">$")) {
1465 // looks like a trust scope regular expression created by gpg
1466 auto domain = scopeRegExp.mid(10, scopeRegExp.size() - 10 - 2);
1467 domain.replace(escapedNonAlphaNum, QStringLiteral(R"(\1)"));
1468 return domain;
1469 }
1470 return scopeRegExp;
1471}
1472}
1473
1474QString Formatting::trustSignatureDomain(const GpgME::UserID::Signature &sig)
1475{
1476 return formatTrustScope(sig.trustScope());
1477}
1478
1479QString Formatting::trustSignature(const GpgME::UserID::Signature &sig)
1480{
1481 switch (sig.trustValue()) {
1482 case TrustSignatureTrust::Partial:
1483 return i18nc("Certifies this key as partially trusted introducer for 'domain name'.",
1484 "Certifies this key as partially trusted introducer for '%1'.",
1485 trustSignatureDomain(sig));
1486 case TrustSignatureTrust::Complete:
1487 return i18nc("Certifies this key as fully trusted introducer for 'domain name'.",
1488 "Certifies this key as fully trusted introducer for '%1'.",
1489 trustSignatureDomain(sig));
1490 default:
1491 return {};
1492 }
1493}
1494
1495QString Formatting::errorAsString(const GpgME::Error &error)
1496{
1497#ifdef Q_OS_WIN
1498 // On Windows, we set GpgME resp. libgpg-error to return (translated) error messages as UTF-8
1499#if GPGMEPP_ERROR_HAS_ASSTDSTRING
1500 const std::string s = error.asStdString();
1501 qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1);
1502 qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s;
1503 qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray::fromStdString(s).toPercentEncoding();
1504 return QString::fromStdString(s);
1505#else
1506 const char *s = error.asString();
1507 qCDebug(LIBKLEO_LOG) << __func__ << "gettext_use_utf8(-1) returns" << gettext_use_utf8(-1);
1508 qCDebug(LIBKLEO_LOG) << __func__ << "error:" << s;
1509 qCDebug(LIBKLEO_LOG) << __func__ << "error (percent-encoded):" << QByteArray{s}.toPercentEncoding();
1510 return QString::fromUtf8(s);
1511#endif
1512#else
1513#if GPGMEPP_ERROR_HAS_ASSTDSTRING
1514 const std::string s = error.asStdString();
1515 return QString::fromLocal8Bit(QByteArrayView{s.data(), qsizetype(s.size())});
1516#else
1517 return QString::fromLocal8Bit(error.asString());
1518#endif
1519#endif
1520}
1521
1522QString Formatting::prettyAlgorithmName(const std::string &algorithm)
1523{
1524 static const std::map<std::string, QString> displayNames = {
1525 {"brainpoolP256r1", i18nc("@info", "ECC (Brainpool P-256)")},
1526 {"brainpoolP384r1", i18nc("@info", "ECC (Brainpool P-384)")},
1527 {"brainpoolP512r1", i18nc("@info", "ECC (Brainpool P-512)")},
1528 {"curve25519", i18nc("@info", "ECC (Curve25519)")},
1529 {"curve448", i18nc("@info", "ECC (Curve448)")},
1530 {"ed25519", i18nc("@info", "ECC (Ed25519)")},
1531 {"ed448", i18nc("@info", "ECC (Ed448)")},
1532 {"cv25519", i18nc("@info", "ECC (Cv25519)")},
1533 {"cv448", i18nc("@info", "ECC (Cv448)")},
1534 {"nistp256", i18nc("@info", "ECC (NIST P-256)")},
1535 {"nistp384", i18nc("@info", "ECC (NIST P-384)")},
1536 {"nistp521", i18nc("@info", "ECC (NIST P-521)")},
1537 {"rsa2048", i18nc("@info", "RSA 2048")},
1538 {"rsa3072", i18nc("@info", "RSA 3072")},
1539 {"rsa4096", i18nc("@info", "RSA 4096")},
1540 {"dsa1024", i18nc("@info", "DSA 1024")},
1541 {"dsa2048", i18nc("@info", "DSA 2048")},
1542 {"elg1024", i18nc("@info", "Elgamal 1024")},
1543 {"elg2048", i18nc("@info", "Elgamal 2048")},
1544 {"elg3072", i18nc("@info", "Elgamal 3072")},
1545 {"elg4096", i18nc("@info", "Elgamal 4096")},
1546 };
1547 const auto it = displayNames.find(algorithm);
1548 return (it != displayNames.end()) ? it->second : i18nc("@info", "Unknown algorithm");
1549}
1550
1551static QString formatValidSignatureWithTrustLevel(const GpgME::UserID &id)
1552{
1553 if (id.isNull()) {
1554 return QString();
1555 }
1556 switch (id.validity()) {
1557 case GpgME::UserID::Marginal:
1558 return i18n("The signature is valid but the trust in the certificate's validity is only marginal.");
1559 case GpgME::UserID::Full:
1560 return i18n("The signature is valid and the certificate's validity is fully trusted.");
1561 case GpgME::UserID::Ultimate:
1562 return i18n("The signature is valid and the certificate's validity is ultimately trusted.");
1563 case GpgME::UserID::Never:
1564 return i18n("The signature is valid but the certificate's validity is <em>not trusted</em>.");
1565 case GpgME::UserID::Unknown:
1566 return i18n("The signature is valid but the certificate's validity is unknown.");
1567 case GpgME::UserID::Undefined:
1568 default:
1569 return i18n("The signature is valid but the certificate's validity is undefined.");
1570 }
1571}
1572
1573static QString renderKeyLink(const QString &fpr, const QString &text)
1574{
1575 return QStringLiteral("<a href=\"key:%1\">%2</a>").arg(fpr, text.toHtmlEscaped());
1576}
1577
1578static QString renderKey(const GpgME::Key &key)
1579{
1580 if (key.isNull()) {
1581 return i18n("Unknown certificate");
1582 }
1583
1584 return renderKeyLink(QLatin1StringView(key.primaryFingerprint()),
1585 i18nc("User ID (Key ID)", "%1 (%2)", Formatting::prettyNameAndEMail(key), Formatting::prettyID(key.subkey(0).keyID())));
1586}
1587
1588static QString formatSigningInformation(const GpgME::Signature &sig, const GpgME::Key &key)
1589{
1590 if (sig.isNull()) {
1591 return QString();
1592 }
1593 QString text;
1594 const QDateTime dt = sig.creationTime() != 0 ? QDateTime::fromSecsSinceEpoch(quint32(sig.creationTime())) : QDateTime();
1595
1596 if (key.isNull()) {
1597 const QString id = u"<br/>"_s + Formatting::prettyID(sig.fingerprint());
1598 if (dt.isValid()) {
1599 return i18nc("1 is a date", "Signature created on %1 using an unknown certificate with fingerprint %2", QLocale().toString(dt), id);
1600 }
1601 return i18n("Signature created using an unknown certificate with fingerprint %1", id);
1602 }
1603
1604 if (dt.isValid()) {
1605 text += i18nc("1 is a date", "Signature created on %1 with certificate: %2", QLocale().toString(dt), renderKey(key));
1606 } else {
1607 text += i18n("Signature created with certificate: %1", renderKey(key));
1608 }
1609
1610 if (Kleo::DeVSCompliance::isCompliant() && ((sig.summary() & GpgME::Signature::Valid) || (sig.summary() & GpgME::Signature::Green))) {
1611 text += (QStringLiteral("<br/>")
1612 + (sig.isDeVs() ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
1613 "The signature is %1",
1614 Kleo::DeVSCompliance::name(true))
1615 : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
1616 "The signature <b>is not</b> %1.",
1617 Kleo::DeVSCompliance::name(true))));
1618 }
1619
1620 return text;
1621}
1622
1623static QString signatureSummaryToString(GpgME::Signature::Summary summary)
1624{
1625 if (summary & GpgME::Signature::None) {
1626 return i18n("Error: Signature not verified");
1627 } else if ((summary & GpgME::Signature::Valid) || (summary & GpgME::Signature::Green)) {
1628 return i18n("Good signature");
1629 } else if (summary & GpgME::Signature::KeyRevoked) {
1630 return i18n("Signing certificate was revoked");
1631 } else if (summary & GpgME::Signature::KeyExpired) {
1632 return i18n("Signing certificate is expired");
1633 } else if (summary & GpgME::Signature::KeyMissing) {
1634 return i18n("Certificate is not available");
1635 } else if (summary & GpgME::Signature::SigExpired) {
1636 return i18n("Signature expired");
1637 } else if (summary & GpgME::Signature::CrlMissing) {
1638 return i18n("CRL missing");
1639 } else if (summary & GpgME::Signature::CrlTooOld) {
1640 return i18n("CRL too old");
1641 } else if (summary & GpgME::Signature::BadPolicy) {
1642 return i18n("Bad policy");
1643 } else if (summary & GpgME::Signature::SysError) {
1644 return i18n("System error"); // ### retrieve system error details?
1645 } else if (summary & GpgME::Signature::Red) {
1646 return i18n("Bad signature");
1647 }
1648 return QString();
1649}
1650
1651static QLatin1StringView stripAngleBrackets(const QLatin1StringView &str)
1652{
1653 if (str.isEmpty()) {
1654 return str;
1655 }
1656 if (str[0] == '<' && str[str.size() - 1] == '>') {
1657 return str.mid(1, str.size() - 2);
1658 }
1659 return str;
1660}
1661
1662QString Formatting::email(const GpgME::UserID &uid)
1663{
1664 if (uid.parent().protocol() == GpgME::OpenPGP) {
1665 const QLatin1StringView email(uid.email());
1666 if (!email.isEmpty()) {
1667 return stripAngleBrackets(email).toString();
1668 }
1669 return {};
1670 }
1671
1672 Q_ASSERT(uid.parent().protocol() == GpgME::CMS);
1673
1674 const QLatin1StringView id(uid.id());
1675 if (!id.isEmpty()) {
1676 if (id[0] == '<') {
1677 return stripAngleBrackets(id).toString();
1678 }
1679 return Kleo::DN(id)[QStringLiteral("EMAIL")].trimmed();
1680 }
1681 return {};
1682}
1683
1684static GpgME::UserID findUserIDByMailbox(const GpgME::Key &key, const QString &email)
1685{
1686 const auto userIDs{key.userIDs()};
1687 for (const GpgME::UserID &id : userIDs) {
1688 if (Formatting::email(id).compare(email, Qt::CaseInsensitive)) {
1689 return id;
1690 }
1691 }
1692 return {};
1693}
1694
1695QString Kleo::Formatting::prettySignature(const GpgME::Signature &sig, const QString &sender)
1696{
1697 if (sig.isNull()) {
1698 return QString();
1699 }
1700
1701 const GpgME::Key key = Kleo::KeyCache::instance()->findSigner(sig);
1702
1703 const QString text = formatSigningInformation(sig, key) + QLatin1StringView("<br/>");
1704
1705 // Green
1706 if (sig.summary() & GpgME::Signature::Valid) {
1707 GpgME::UserID id = findUserIDByMailbox(key, sender);
1708 if (id.isNull()) {
1709 for (int i = 0, count = key.userIDs().size(); i < count; i++) {
1710 id = key.userID(i);
1711 if (!id.isNull()) {
1712 break;
1713 }
1714 }
1715 }
1716
1717 return text + formatValidSignatureWithTrustLevel(!id.isNull() ? id : key.userID(0));
1718 }
1719
1720 // Red
1721 if ((sig.summary() & GpgME::Signature::Red)) {
1722 const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
1723 if (sig.summary() & GpgME::Signature::SysError) {
1724 return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status()));
1725 }
1726 return ret;
1727 }
1728
1729 // Key missing
1730 if ((sig.summary() & GpgME::Signature::KeyMissing)) {
1731 return text + i18n("You can search the certificate on a keyserver or import it from a file.");
1732 }
1733
1734 // Yellow
1735 if ((sig.validity() & GpgME::Signature::Validity::Undefined) //
1736 || (sig.validity() & GpgME::Signature::Validity::Unknown) //
1737 || (sig.summary() == GpgME::Signature::Summary::None)) {
1738 return text
1739 + (key.protocol() == GpgME::OpenPGP
1740 ? i18n("The used key is not certified by you or any trusted person.")
1741 : i18n("The used certificate is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown."));
1742 }
1743
1744 // Catch all fall through
1745 const QString ret = text + i18n("The signature is invalid: %1", signatureSummaryToString(sig.summary()));
1746 if (sig.summary() & GpgME::Signature::SysError) {
1747 return ret + QStringLiteral(" (%1)").arg(Kleo::Formatting::errorAsString(sig.status()));
1748 }
1749 return ret;
1750}
DN parser and reorderer.
Definition dn.h:27
QString prettyDN() const
Definition dn.cpp:445
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 Mon Nov 4 2024 16:29:01 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.