1 /*  -*- c++ -*-
2     newkeyapprovaldialog.cpp
3 
4     This file is part of libkleopatra, the KDE keymanagement library
5     SPDX-FileCopyrightText: 2018 Intevation GmbH
6     SPDX-FileCopyrightText: 2021 g10 Code GmbH
7     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
8 
9     SPDX-License-Identifier: GPL-2.0-or-later
10 */
11 
12 #include "newkeyapprovaldialog.h"
13 
14 #include "keyselectioncombo.h"
15 #include "progressdialog.h"
16 #include "kleo/defaultkeyfilter.h"
17 #include "utils/formatting.h"
18 #include "utils/gnupg.h"
19 
20 #include <KLocalizedString>
21 #include <KMessageBox>
22 
23 #include <QApplication>
24 #include <QButtonGroup>
25 #include <QCheckBox>
26 #include <QDesktopWidget>
27 #include <QDialogButtonBox>
28 #include <QGroupBox>
29 #include <QHBoxLayout>
30 #include <QLabel>
31 #include <QMap>
32 #include <QPushButton>
33 #include <QRadioButton>
34 #include <QScrollArea>
35 #include <QToolTip>
36 #include <QVBoxLayout>
37 
38 #include <QGpgME/DefaultKeyGenerationJob>
39 #include <QGpgME/Job>
40 
41 #include <gpgme++/keygenerationresult.h>
42 #include <gpgme++/key.h>
43 
44 #include "libkleo_debug.h"
45 
46 using namespace Kleo;
47 using namespace GpgME;
48 
operator <<(QDebug debug,const GpgME::Key & key)49 QDebug operator<<(QDebug debug, const GpgME::Key &key)
50 {
51     if (key.isNull()) {
52         debug << "Null";
53     } else {
54         debug << Formatting::summaryLine(key);
55     }
56     return debug.maybeSpace();
57 }
58 
59 namespace {
60 class EncryptFilter: public DefaultKeyFilter
61 {
62 public:
EncryptFilter()63     EncryptFilter() : DefaultKeyFilter()
64     {
65         setCanEncrypt(DefaultKeyFilter::Set);
66     }
67 };
68 static std::shared_ptr<KeyFilter> s_encryptFilter = std::shared_ptr<KeyFilter>(new EncryptFilter);
69 
70 class OpenPGPFilter: public DefaultKeyFilter
71 {
72 public:
OpenPGPFilter()73     OpenPGPFilter() : DefaultKeyFilter()
74     {
75         setIsOpenPGP(DefaultKeyFilter::Set);
76         setCanEncrypt(DefaultKeyFilter::Set);
77     }
78 };
79 static std::shared_ptr<KeyFilter> s_pgpEncryptFilter = std::shared_ptr<KeyFilter> (new OpenPGPFilter);
80 
81 class OpenPGPSignFilter: public DefaultKeyFilter
82 {
83 public:
OpenPGPSignFilter()84     OpenPGPSignFilter() : DefaultKeyFilter()
85     {
86         /* Also list unusable keys to make it transparent why they are unusable */
87         setDisabled(DefaultKeyFilter::NotSet);
88         setRevoked(DefaultKeyFilter::NotSet);
89         setExpired(DefaultKeyFilter::NotSet);
90         setCanSign(DefaultKeyFilter::Set);
91         setHasSecret(DefaultKeyFilter::Set);
92         setIsOpenPGP(DefaultKeyFilter::Set);
93     }
94 };
95 static std::shared_ptr<KeyFilter> s_pgpSignFilter = std::shared_ptr<KeyFilter> (new OpenPGPSignFilter);
96 
97 class SMIMEFilter: public DefaultKeyFilter
98 {
99 public:
SMIMEFilter()100     SMIMEFilter(): DefaultKeyFilter()
101     {
102         setIsOpenPGP(DefaultKeyFilter::NotSet);
103         setCanEncrypt(DefaultKeyFilter::Set);
104     }
105 };
106 static std::shared_ptr<KeyFilter> s_smimeEncryptFilter = std::shared_ptr<KeyFilter> (new SMIMEFilter);
107 
108 class SMIMESignFilter: public DefaultKeyFilter
109 {
110 public:
SMIMESignFilter()111     SMIMESignFilter(): DefaultKeyFilter()
112     {
113         setDisabled(DefaultKeyFilter::NotSet);
114         setRevoked(DefaultKeyFilter::NotSet);
115         setExpired(DefaultKeyFilter::NotSet);
116         setCanSign(DefaultKeyFilter::Set);
117         setIsOpenPGP(DefaultKeyFilter::NotSet);
118         setHasSecret(DefaultKeyFilter::Set);
119     }
120 };
121 static std::shared_ptr<KeyFilter> s_smimeSignFilter = std::shared_ptr<KeyFilter> (new SMIMESignFilter);
122 
123 /* Some decoration and a button to remove the filter for a keyselectioncombo */
124 class ComboWidget: public QWidget
125 {
126     Q_OBJECT
127 public:
ComboWidget(KeySelectionCombo * combo)128     explicit ComboWidget(KeySelectionCombo *combo):
129         mCombo(combo),
130         mFilterBtn(new QPushButton)
131     {
132         auto hLay = new QHBoxLayout(this);
133         auto infoBtn = new QPushButton;
134         infoBtn->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual")));
135         infoBtn->setIconSize(QSize(22,22));
136         infoBtn->setFlat(true);
137         hLay->addWidget(infoBtn);
138         hLay->addWidget(combo, 1);
139         hLay->addWidget(mFilterBtn, 0);
140 
141         connect(infoBtn, &QPushButton::clicked, this, [this, infoBtn] () {
142             QToolTip::showText(infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0),
143                     mCombo->currentData(Qt::ToolTipRole).toString(), infoBtn, QRect(), 30000);
144         });
145 
146         // FIXME: This is ugly to enforce but otherwise the
147         // icon is broken.
148         combo->setMinimumHeight(22);
149         mFilterBtn->setMinimumHeight(23);
150 
151         updateFilterButton();
152 
153         connect(mFilterBtn, &QPushButton::clicked, this, [this] () {
154             const QString curFilter = mCombo->idFilter();
155             if (curFilter.isEmpty()) {
156                 setIdFilter(mLastIdFilter);
157                 mLastIdFilter = QString();
158             } else {
159                 setIdFilter(QString());
160                 mLastIdFilter = curFilter;
161             }
162         });
163     }
164 
setIdFilter(const QString & id)165     void setIdFilter(const QString &id)
166     {
167         mCombo->setIdFilter(id);
168         updateFilterButton();
169     }
170 
updateFilterButton()171     void updateFilterButton()
172     {
173         if (mCombo->idFilter().isEmpty()) {
174             mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-add-filters")));
175             mFilterBtn->setToolTip(i18n("Show keys matching the email address"));
176         } else {
177             mFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("kt-remove-filters")));
178             mFilterBtn->setToolTip(i18n("Show all keys"));
179         }
180     }
181 
combo()182     KeySelectionCombo *combo()
183     {
184         return mCombo;
185     }
186 
fixedProtocol() const187     GpgME::Protocol fixedProtocol() const
188     {
189         return mFixedProtocol;
190     }
191 
setFixedProtocol(GpgME::Protocol proto)192     void setFixedProtocol(GpgME::Protocol proto)
193     {
194         mFixedProtocol = proto;
195     }
196 
197 private:
198     KeySelectionCombo *mCombo;
199     QPushButton *mFilterBtn;
200     QString mLastIdFilter;
201     GpgME::Protocol mFixedProtocol = GpgME::UnknownProtocol;
202 };
203 
keyValidity(const GpgME::Key & key)204 static enum GpgME::UserID::Validity keyValidity(const GpgME::Key &key)
205 {
206     enum GpgME::UserID::Validity validity = GpgME::UserID::Validity::Unknown;
207 
208     for (const auto &uid: key.userIDs()) {
209         if (validity == GpgME::UserID::Validity::Unknown
210             || validity > uid.validity()) {
211             validity = uid.validity();
212         }
213     }
214 
215     return validity;
216 }
217 
key_has_addr(const GpgME::Key & key,const QString & addr)218 static bool key_has_addr(const GpgME::Key &key, const QString &addr)
219 {
220     for (const auto &uid: key.userIDs()) {
221         if (QString::fromStdString(uid.addrSpec()).toLower() == addr.toLower()) {
222             return true;
223         }
224     }
225     return false;
226 }
227 
anyKeyHasProtocol(const std::vector<Key> & keys,Protocol protocol)228 bool anyKeyHasProtocol(const std::vector<Key> &keys, Protocol protocol)
229 {
230     return std::any_of(std::begin(keys), std::end(keys), [protocol] (const auto &key) { return key.protocol() == protocol; });
231 }
232 
findfirstKeyOfType(const std::vector<Key> & keys,Protocol protocol)233 Key findfirstKeyOfType(const std::vector<Key> &keys, Protocol protocol)
234 {
235     const auto it = std::find_if(std::begin(keys), std::end(keys), [protocol] (const auto &key) { return key.protocol() == protocol; });
236     return it != std::end(keys) ? *it : Key();
237 }
238 
239 } // namespace
240 
241 class NewKeyApprovalDialog::Private
242 {
243 private:
244     enum Action {
245         Unset,
246         GenerateKey,
247         IgnoreKey,
248     };
249 public:
250     enum {
251         OpenPGPButtonId = 1,
252         SMIMEButtonId = 2
253     };
254 
Private(NewKeyApprovalDialog * qq,bool encrypt,bool sign,GpgME::Protocol forcedProtocol,GpgME::Protocol presetProtocol,const QString & sender,bool allowMixed)255     Private(NewKeyApprovalDialog *qq,
256             bool encrypt,
257             bool sign,
258             GpgME::Protocol forcedProtocol,
259             GpgME::Protocol presetProtocol,
260             const QString &sender,
261             bool allowMixed)
262         : mForcedProtocol{forcedProtocol}
263         , mSender{sender}
264         , mSign{sign}
265         , mEncrypt{encrypt}
266         , mAllowMixed{allowMixed}
267         , q{qq}
268     {
269         Q_ASSERT(forcedProtocol == GpgME::UnknownProtocol || presetProtocol == GpgME::UnknownProtocol || presetProtocol == forcedProtocol);
270         Q_ASSERT(!allowMixed || (allowMixed && forcedProtocol == GpgME::UnknownProtocol));
271         Q_ASSERT(!(!allowMixed && presetProtocol == GpgME::UnknownProtocol));
272 
273         // We do the translation here to avoid having the same string multiple times.
274         mGenerateTooltip = i18nc("@info:tooltip for a 'Generate new key pair' action "
275                                  "in a combobox when a user does not yet have an OpenPGP or S/MIME key.",
276                                  "Generate a new key using your E-Mail address.<br/><br/>"
277                                  "The key is necessary to decrypt and sign E-Mails. "
278                                  "You will be asked for a passphrase to protect this key and the protected key "
279                                  "will be stored in your home directory.");
280         mMainLay = new QVBoxLayout;
281 
282         QDialogButtonBox *btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
283         mOkButton = btnBox->button(QDialogButtonBox::Ok);
284 #ifndef NDEBUG
285         mOkButton->setObjectName(QStringLiteral("ok button"));
286 #endif
__anon713252920702() 287         QObject::connect (btnBox, &QDialogButtonBox::accepted, q, [this] () {
288                 accepted();
289             });
290         QObject::connect (btnBox, &QDialogButtonBox::rejected, q, &QDialog::reject);
291 
292         mScrollArea = new QScrollArea;
293         mScrollArea->setWidget(new QWidget);
294         mScrollLayout = new QVBoxLayout;
295         mScrollArea->widget()->setLayout(mScrollLayout);
296         mScrollArea->setWidgetResizable(true);
297         mScrollArea->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
298         mScrollArea->setFrameStyle(QFrame::NoFrame);
299         mScrollLayout->setContentsMargins(0, 0, 0, 0);
300 
301         q->setWindowTitle(i18nc("@title:window", "Security approval"));
302 
303         auto fmtLayout = new QHBoxLayout;
304         mFormatBtns = new QButtonGroup(qq);
305         QAbstractButton *pgpBtn;
306         QAbstractButton *smimeBtn;
307         if (mAllowMixed) {
308             pgpBtn = new QCheckBox(i18n("OpenPGP"));
309             smimeBtn = new QCheckBox(i18n("S/MIME"));
310         } else {
311             pgpBtn = new QRadioButton(i18n("OpenPGP"));
312             smimeBtn = new QRadioButton(i18n("S/MIME"));
313         }
314 #ifndef NDEBUG
315         pgpBtn->setObjectName(QStringLiteral("openpgp button"));
316         smimeBtn->setObjectName(QStringLiteral("smime button"));
317 #endif
318         mFormatBtns->addButton(pgpBtn, OpenPGPButtonId);
319         mFormatBtns->addButton(smimeBtn, SMIMEButtonId);
320         mFormatBtns->setExclusive(!mAllowMixed);
321 
322         fmtLayout->addStretch(-1);
323         fmtLayout->addWidget(pgpBtn);
324         fmtLayout->addWidget(smimeBtn);
325         mMainLay->addLayout(fmtLayout);
326 
327         if (mForcedProtocol != GpgME::UnknownProtocol) {
328             pgpBtn->setChecked(mForcedProtocol == GpgME::OpenPGP);
329             smimeBtn->setChecked(mForcedProtocol == GpgME::CMS);
330             pgpBtn->setVisible(false);
331             smimeBtn->setVisible(false);
332         } else {
333             pgpBtn->setChecked(presetProtocol == GpgME::OpenPGP || presetProtocol == GpgME::UnknownProtocol);
334             smimeBtn->setChecked(presetProtocol == GpgME::CMS || presetProtocol == GpgME::UnknownProtocol);
335         }
336 
337         QObject::connect(mFormatBtns, &QButtonGroup::idClicked,
__anon713252920802(int buttonId) 338                          q, [this](int buttonId) {
339                              // ensure that at least one protocol button is checked
340                              if (mAllowMixed
341                                     && !mFormatBtns->button(OpenPGPButtonId)->isChecked()
342                                     && !mFormatBtns->button(SMIMEButtonId)->isChecked()) {
343                                  mFormatBtns->button(buttonId == OpenPGPButtonId ? SMIMEButtonId : OpenPGPButtonId)->setChecked(true);
344                              }
345                              updateWidgets();
346                         });
347 
348         mMainLay->addWidget(mScrollArea);
349 
350         mComplianceLbl = new QLabel;
351         mComplianceLbl->setVisible(false);
352 #ifndef NDEBUG
353         mComplianceLbl->setObjectName(QStringLiteral("compliance label"));
354 #endif
355 
356         auto btnLayout = new QHBoxLayout;
357         btnLayout->addWidget(mComplianceLbl);
358         btnLayout->addWidget(btnBox);
359         mMainLay->addLayout(btnLayout);
360 
361         q->setLayout(mMainLay);
362     }
363 
364     ~Private() = default;
365 
currentProtocol()366     Protocol currentProtocol()
367     {
368         const bool openPGPButtonChecked = mFormatBtns->button(OpenPGPButtonId)->isChecked();
369         const bool smimeButtonChecked = mFormatBtns->button(SMIMEButtonId)->isChecked();
370         if (mAllowMixed) {
371             if (openPGPButtonChecked && !smimeButtonChecked) {
372                 return OpenPGP;
373             }
374             if (!openPGPButtonChecked && smimeButtonChecked) {
375                 return CMS;
376             }
377         } else if (openPGPButtonChecked) {
378             return OpenPGP;
379         } else if (smimeButtonChecked) {
380             return CMS;
381         }
382         return UnknownProtocol;
383     }
384 
findVisibleKeySelectionComboWithGenerateKey()385     auto findVisibleKeySelectionComboWithGenerateKey()
386     {
387         const auto it = std::find_if(std::begin(mAllCombos), std::end(mAllCombos),
388                                      [] (auto combo) {
389                                          return combo->isVisible()
390                                              && combo->currentData(Qt::UserRole).toInt() == GenerateKey;
391                                      });
392         return it != std::end(mAllCombos) ? *it : nullptr;
393     }
394 
generateKey(KeySelectionCombo * combo)395     void generateKey(KeySelectionCombo *combo)
396     {
397         const auto &addr = combo->property("address").toString();
398         auto job = new QGpgME::DefaultKeyGenerationJob(q);
399         auto progress = new Kleo::ProgressDialog(job, i18n("Generating key for '%1'...", addr) + QStringLiteral("\n\n") +
400                                                  i18n("This can take several minutes."), q);
401         progress->setWindowFlags(progress->windowFlags() & ~Qt::WindowContextHelpButtonHint);
402         progress->setWindowTitle(i18nc("@title:window", "Key generation"));
403         progress->setModal(true);
404         progress->setAutoClose(true);
405         progress->setMinimumDuration(0);
406         progress->setValue(0);
407 
408         mRunningJobs << job;
409         connect (job, &QGpgME::DefaultKeyGenerationJob::result, q,
410             [this, job, combo] (const GpgME::KeyGenerationResult &result) {
411                 handleKeyGenResult(result, job, combo);
412             });
413         job->start(addr, QString());
414         return;
415     }
416 
handleKeyGenResult(const GpgME::KeyGenerationResult & result,QGpgME::Job * job,KeySelectionCombo * combo)417     void handleKeyGenResult(const GpgME::KeyGenerationResult &result, QGpgME::Job *job, KeySelectionCombo *combo)
418     {
419         mLastError = result.error();
420         if (!mLastError || mLastError.isCanceled()) {
421             combo->setDefaultKey(QString::fromLatin1(result.fingerprint()), GpgME::OpenPGP);
422             connect (combo, &KeySelectionCombo::keyListingFinished, q, [this, job] () {
423                     mRunningJobs.removeAll(job);
424                 });
425             combo->refreshKeys();
426         } else {
427             mRunningJobs.removeAll(job);
428         }
429     }
430 
checkAccepted()431     void checkAccepted()
432     {
433         if (mLastError || mLastError.isCanceled()) {
434             KMessageBox::error(q, QString::fromLocal8Bit(mLastError.asString()), i18n("Operation Failed"));
435             mRunningJobs.clear();
436             return;
437         }
438 
439         if (!mRunningJobs.empty()) {
440             return;
441         }
442 
443         /* Save the keys */
444         mAcceptedResult.protocol = currentProtocol();
445         for (const auto combo: std::as_const(mEncCombos)) {
446             const auto addr = combo->property("address").toString();
447             const auto key = combo->currentKey();
448             if (!combo->isVisible() || key.isNull()) {
449                 continue;
450             }
451             mAcceptedResult.encryptionKeys[addr].push_back(key);
452         }
453         for (const auto combo: std::as_const(mSigningCombos)) {
454             const auto key = combo->currentKey();
455             if (!combo->isVisible() || key.isNull()) {
456                 continue;
457             }
458             mAcceptedResult.signingKeys.push_back(key);
459         }
460 
461         q->accept();
462     }
463 
accepted()464     void accepted()
465     {
466         // We can assume everything was validly resolved, otherwise
467         // the OK button would have been disabled.
468         // Handle custom items now.
469         if (auto combo = findVisibleKeySelectionComboWithGenerateKey()) {
470             generateKey(combo);
471             return;
472         }
473         checkAccepted();
474     }
475 
encryptionKeyFilter(Protocol protocol)476     auto encryptionKeyFilter(Protocol protocol)
477     {
478         switch (protocol) {
479         case OpenPGP:
480             return s_pgpEncryptFilter;
481         case CMS:
482             return s_smimeEncryptFilter;
483         default:
484             return s_encryptFilter;
485         }
486     }
487 
updateWidgets()488     void updateWidgets()
489     {
490         const Protocol protocol = currentProtocol();
491         const auto encryptionFilter = encryptionKeyFilter(protocol);
492 
493         for (auto combo: std::as_const(mSigningCombos)) {
494             auto widget = qobject_cast<ComboWidget *>(combo->parentWidget());
495             if (!widget) {
496                 qCDebug(LIBKLEO_LOG) << "Failed to find signature combo widget";
497                 continue;
498             }
499             widget->setVisible(protocol == UnknownProtocol || widget->fixedProtocol() == UnknownProtocol || widget->fixedProtocol() == protocol);
500         }
501         for (auto combo: std::as_const(mEncCombos)) {
502             auto widget = qobject_cast<ComboWidget *>(combo->parentWidget());
503             if (!widget) {
504                 qCDebug(LIBKLEO_LOG) << "Failed to find combo widget";
505                 continue;
506             }
507             widget->setVisible(protocol == UnknownProtocol || widget->fixedProtocol() == UnknownProtocol || widget->fixedProtocol() == protocol);
508             if (widget->isVisible() && combo->property("address") != mSender) {
509                 combo->setKeyFilter(encryptionFilter);
510             }
511         }
512         // hide the labels indicating the protocol of the sender's keys if only a single protocol is active
513         const auto protocolLabels = q->findChildren<QLabel *>(QStringLiteral("protocol label"));
514         for (auto label: protocolLabels) {
515             label->setVisible(protocol == UnknownProtocol);
516         }
517     }
518 
createProtocolLabel(Protocol protocol)519     auto createProtocolLabel(Protocol protocol)
520     {
521         auto label = new QLabel(Formatting::displayName(protocol));
522         label->setObjectName(QStringLiteral("protocol label"));
523         return label;
524     }
525 
createSigningCombo(const QString & addr,const GpgME::Key & key,Protocol protocol=UnknownProtocol)526     ComboWidget *createSigningCombo(const QString &addr, const GpgME::Key &key, Protocol protocol = UnknownProtocol)
527     {
528         Q_ASSERT(!key.isNull() || protocol != UnknownProtocol);
529         protocol = !key.isNull() ? key.protocol() : protocol;
530 
531         auto combo = new KeySelectionCombo();
532         auto comboWidget = new ComboWidget(combo);
533 #ifndef NDEBUG
534         combo->setObjectName(QStringLiteral("signing key"));
535 #endif
536         if (protocol == GpgME::OpenPGP) {
537             combo->setKeyFilter(s_pgpSignFilter);
538         } else if (protocol == GpgME::CMS) {
539             combo->setKeyFilter(s_smimeSignFilter);
540         }
541         if (key.isNull() || key_has_addr(key, mSender)) {
542             comboWidget->setIdFilter(mSender);
543         }
544         comboWidget->setFixedProtocol(protocol);
545         if (!key.isNull()) {
546             combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), protocol);
547         }
548         if (key.isNull() && protocol == OpenPGP) {
549             combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")),
550                                     i18n("Generate a new key pair"), GenerateKey,
551                                     mGenerateTooltip);
552         }
553         combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("emblem-unavailable")),
554                 i18n("Don't confirm identity and integrity"), IgnoreKey,
555                 i18nc("@info:tooltip for not selecting a key for signing.",
556                       "The E-Mail will not be cryptographically signed."));
557 
558         mSigningCombos << combo;
559         mAllCombos << combo;
560         combo->setProperty("address", addr);
561 
562         connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this] () {
563             updateOkButton();
564         });
565         connect(combo, qOverload<int>(&QComboBox::currentIndexChanged), q, [this] () {
566             updateOkButton();
567         });
568 
569         return comboWidget;
570     }
571 
setSigningKeys(const std::vector<GpgME::Key> & preferredKeys,const std::vector<GpgME::Key> & alternativeKeys)572     void setSigningKeys(const std::vector<GpgME::Key> &preferredKeys, const std::vector<GpgME::Key> &alternativeKeys)
573     {
574         auto group = new QGroupBox(i18nc("Caption for signing key selection", "Confirm identity '%1' as:", mSender));
575         group->setAlignment(Qt::AlignLeft);
576         auto sigLayout = new QVBoxLayout(group);
577 
578         const bool mayNeedOpenPGP = mForcedProtocol != CMS;
579         const bool mayNeedCMS = mForcedProtocol != OpenPGP;
580         if (mayNeedOpenPGP) {
581             if (mAllowMixed) {
582                 sigLayout->addWidget(createProtocolLabel(OpenPGP));
583             }
584             const Key preferredKey = findfirstKeyOfType(preferredKeys, OpenPGP);
585             const Key alternativeKey = findfirstKeyOfType(alternativeKeys, OpenPGP);
586             if (!preferredKey.isNull()) {
587                 qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey;
588                 auto comboWidget = createSigningCombo(mSender, preferredKey);
589                 sigLayout->addWidget(comboWidget);
590             } else if (!alternativeKey.isNull()) {
591                 qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey;
592                 auto comboWidget = createSigningCombo(mSender, alternativeKey);
593                 sigLayout->addWidget(comboWidget);
594             } else {
595                 qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for OpenPGP key";
596                 auto comboWidget = createSigningCombo(mSender, Key(), OpenPGP);
597                 sigLayout->addWidget(comboWidget);
598             }
599         }
600         if (mayNeedCMS) {
601             if (mAllowMixed) {
602                 sigLayout->addWidget(createProtocolLabel(CMS));
603             }
604             const Key preferredKey = findfirstKeyOfType(preferredKeys, CMS);
605             const Key alternativeKey = findfirstKeyOfType(alternativeKeys, CMS);
606             if (!preferredKey.isNull()) {
607                 qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << preferredKey;
608                 auto comboWidget = createSigningCombo(mSender, preferredKey);
609                 sigLayout->addWidget(comboWidget);
610             } else if (!alternativeKey.isNull()) {
611                 qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for" << alternativeKey;
612                 auto comboWidget = createSigningCombo(mSender, alternativeKey);
613                 sigLayout->addWidget(comboWidget);
614             } else {
615                 qCDebug(LIBKLEO_LOG) << "setSigningKeys - creating signing combo for S/MIME key";
616                 auto comboWidget = createSigningCombo(mSender, Key(), CMS);
617                 sigLayout->addWidget(comboWidget);
618             }
619         }
620 
621         mScrollLayout->addWidget(group);
622     }
623 
createEncryptionCombo(const QString & addr,const GpgME::Key & key,Protocol fixedProtocol)624     ComboWidget *createEncryptionCombo(const QString &addr, const GpgME::Key &key, Protocol fixedProtocol)
625     {
626         auto combo = new KeySelectionCombo(false);
627         auto comboWidget = new ComboWidget(combo);
628 #ifndef NDEBUG
629         combo->setObjectName(QStringLiteral("encryption key"));
630 #endif
631         if (fixedProtocol == GpgME::OpenPGP) {
632             combo->setKeyFilter(s_pgpEncryptFilter);
633         } else if (fixedProtocol == GpgME::CMS) {
634             combo->setKeyFilter(s_smimeEncryptFilter);
635         } else {
636             combo->setKeyFilter(s_encryptFilter);
637         }
638         if (key.isNull() || key_has_addr (key, addr)) {
639             comboWidget->setIdFilter(addr);
640         }
641         comboWidget->setFixedProtocol(fixedProtocol);
642         if (!key.isNull()) {
643             combo->setDefaultKey(QString::fromLatin1(key.primaryFingerprint()), fixedProtocol);
644         }
645 
646         if (addr == mSender && key.isNull() && fixedProtocol == OpenPGP) {
647             combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")),
648                                     i18n("Generate a new key pair"), GenerateKey,
649                                     mGenerateTooltip);
650         }
651 
652         combo->appendCustomItem(QIcon::fromTheme(QStringLiteral("emblem-unavailable")),
653                 i18n("No key. Recipient will be unable to decrypt."), IgnoreKey,
654                 i18nc("@info:tooltip for No Key selected for a specific recipient.",
655                         "Do not select a key for this recipient.<br/><br/>"
656                         "The recipient will receive the encrypted E-Mail, but it can only "
657                         "be decrypted with the other keys selected in this dialog."));
658 
659         connect(combo, &KeySelectionCombo::currentKeyChanged, q, [this] () {
660             updateOkButton();
661         });
662         connect(combo, qOverload<int>(&QComboBox::currentIndexChanged), q, [this] () {
663             updateOkButton();
664         });
665 
666         mEncCombos << combo;
667         mAllCombos << combo;
668         combo->setProperty("address", addr);
669         return comboWidget;
670     }
671 
addEncryptionAddr(const QString & addr,Protocol preferredKeysProtocol,const std::vector<GpgME::Key> & preferredKeys,Protocol alternativeKeysProtocol,const std::vector<GpgME::Key> & alternativeKeys,QGridLayout * encGrid)672     void addEncryptionAddr(const QString &addr,
673                            Protocol preferredKeysProtocol, const std::vector<GpgME::Key> &preferredKeys,
674                            Protocol alternativeKeysProtocol, const std::vector<GpgME::Key> &alternativeKeys,
675                            QGridLayout *encGrid)
676     {
677         if (addr == mSender) {
678             const bool mayNeedOpenPGP = mForcedProtocol != CMS;
679             const bool mayNeedCMS = mForcedProtocol != OpenPGP;
680             if (mayNeedOpenPGP) {
681                 if (mAllowMixed) {
682                     encGrid->addWidget(createProtocolLabel(OpenPGP), encGrid->rowCount(), 0);
683                 }
684                 for (const auto &key : preferredKeys) {
685                     if (key.protocol() == OpenPGP) {
686                         qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key;
687                         auto comboWidget = createEncryptionCombo(addr, key, OpenPGP);
688                         encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
689                     }
690                 }
691                 for (const auto &key : alternativeKeys) {
692                     if (key.protocol() == OpenPGP) {
693                         qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key;
694                         auto comboWidget = createEncryptionCombo(addr, key, OpenPGP);
695                         encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
696                     }
697                 }
698                 if (!anyKeyHasProtocol(preferredKeys, OpenPGP) && !anyKeyHasProtocol(alternativeKeys, OpenPGP)) {
699                     qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for OpenPGP key";
700                     auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), OpenPGP);
701                     encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
702                 }
703             }
704             if (mayNeedCMS) {
705                 if (mAllowMixed) {
706                     encGrid->addWidget(createProtocolLabel(CMS), encGrid->rowCount(), 0);
707                 }
708                 for (const auto &key : preferredKeys) {
709                     if (key.protocol() == CMS) {
710                         qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key;
711                         auto comboWidget = createEncryptionCombo(addr, key, CMS);
712                         encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
713                     }
714                 }
715                 for (const auto &key : alternativeKeys) {
716                     if (key.protocol() == CMS) {
717                         qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key;
718                         auto comboWidget = createEncryptionCombo(addr, key, CMS);
719                         encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
720                     }
721                 }
722                 if (!anyKeyHasProtocol(preferredKeys, CMS) && !anyKeyHasProtocol(alternativeKeys, CMS)) {
723                     qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for S/MIME key";
724                     auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), CMS);
725                     encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
726                 }
727             }
728         } else {
729             encGrid->addWidget(new QLabel(addr), encGrid->rowCount(), 0);
730 
731             for (const auto &key : preferredKeys) {
732                 qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key;
733                 auto comboWidget = createEncryptionCombo(addr, key, preferredKeysProtocol);
734                 encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
735             }
736             for (const auto &key : alternativeKeys) {
737                 qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << key;
738                 auto comboWidget = createEncryptionCombo(addr, key, alternativeKeysProtocol);
739                 encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
740             }
741             if (!mAllowMixed) {
742                 if (preferredKeys.empty()) {
743                     qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(preferredKeysProtocol) << "key";
744                     auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), preferredKeysProtocol);
745                     encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
746                 }
747                 if (alternativeKeys.empty() && alternativeKeysProtocol != UnknownProtocol) {
748                     qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for" << Formatting::displayName(alternativeKeysProtocol) << "key";
749                     auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), alternativeKeysProtocol);
750                     encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
751                 }
752             } else {
753                 if (preferredKeys.empty() && alternativeKeys.empty()) {
754                     qCDebug(LIBKLEO_LOG) << "setEncryptionKeys -" << addr << "- creating encryption combo for any key";
755                     auto comboWidget = createEncryptionCombo(addr, GpgME::Key(), UnknownProtocol);
756                     encGrid->addWidget(comboWidget, encGrid->rowCount(), 0, 1, 2);
757                 }
758             }
759         }
760     }
761 
setEncryptionKeys(Protocol preferredKeysProtocol,const QMap<QString,std::vector<GpgME::Key>> & preferredKeys,Protocol alternativeKeysProtocol,const QMap<QString,std::vector<GpgME::Key>> & alternativeKeys)762     void setEncryptionKeys(Protocol preferredKeysProtocol, const QMap<QString, std::vector<GpgME::Key>> &preferredKeys,
763                            Protocol alternativeKeysProtocol, const QMap<QString, std::vector<GpgME::Key>> &alternativeKeys)
764     {
765         {
766             auto group = new QGroupBox(i18nc("Encrypt to self (email address):", "Encrypt to self (%1):", mSender));
767 #ifndef NDEBUG
768             group->setObjectName(QStringLiteral("encrypt-to-self box"));
769 #endif
770             group->setAlignment(Qt::AlignLeft);
771             auto encGrid = new QGridLayout(group);
772 
773             addEncryptionAddr(mSender, preferredKeysProtocol, preferredKeys.value(mSender), alternativeKeysProtocol, alternativeKeys.value(mSender), encGrid);
774 
775             encGrid->setColumnStretch(1, -1);
776             mScrollLayout->addWidget(group);
777         }
778 
779         const bool hasOtherRecipients = std::any_of(preferredKeys.keyBegin(), preferredKeys.keyEnd(), [this](const auto &recipient) { return recipient != mSender; });
780         if (hasOtherRecipients) {
781             auto group = new QGroupBox(i18n("Encrypt to others:"));
782 #ifndef NDEBUG
783             group->setObjectName(QStringLiteral("encrypt-to-others box"));
784 #endif
785             group->setAlignment(Qt::AlignLeft);
786             auto encGrid = new QGridLayout{group};
787 
788             for (auto it = std::begin(preferredKeys); it != std::end(preferredKeys); ++it) {
789                 const auto &address = it.key();
790                 const auto &keys = it.value();
791                 if (address != mSender) {
792                     addEncryptionAddr(address, preferredKeysProtocol, keys, alternativeKeysProtocol, alternativeKeys.value(address), encGrid);
793                 }
794             }
795 
796             encGrid->setColumnStretch(1, -1);
797             mScrollLayout->addWidget(group);
798         }
799 
800         mScrollLayout->addStretch(-1);
801     }
802 
updateOkButton()803     void updateOkButton()
804     {
805         static QString origOkText = mOkButton->text();
806         const bool isGenerate = bool(findVisibleKeySelectionComboWithGenerateKey());
807         const bool allVisibleEncryptionKeysAreIgnored = std::all_of(std::begin(mEncCombos), std::end(mEncCombos),
808                                                                     [] (auto combo) {
809                                                                         return !combo->isVisible()
810                                                                             || combo->currentData(Qt::UserRole).toInt() == IgnoreKey;
811                                                                     });
812 
813         // If we don't encrypt the ok button is always enabled. But otherwise
814         // we only enable it if we encrypt to at least one recipient.
815         mOkButton->setEnabled(!mEncrypt || !allVisibleEncryptionKeysAreIgnored);
816 
817         mOkButton->setText(isGenerate ? i18n("Generate") : origOkText);
818 
819         if (!Kleo::gnupgUsesDeVsCompliance()) {
820             return;
821         }
822 
823         // Handle compliance
824         bool de_vs = Kleo::gnupgIsDeVsCompliant();
825 
826         if (de_vs) {
827             const Protocol protocol = currentProtocol();
828 
829             for (const auto combo: std::as_const(mAllCombos)) {
830                 if (!combo->isVisible()) {
831                     continue;
832                 }
833                 const auto key = combo->currentKey();
834                 if (key.isNull()) {
835                     continue;
836                 }
837                 if (protocol != UnknownProtocol && key.protocol() != protocol) {
838                     continue;
839                 }
840                 if (!Formatting::isKeyDeVs(key) || keyValidity(key) < GpgME::UserID::Validity::Full) {
841                     de_vs = false;
842                     break;
843                 }
844             }
845         }
846 
847         mOkButton->setIcon(QIcon::fromTheme(de_vs
848                     ? QStringLiteral("security-high")
849                     : QStringLiteral("security-medium")));
850         mOkButton->setStyleSheet(QStringLiteral("background-color: ") + (de_vs
851                     ? QStringLiteral("#D5FAE2")  // KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::PositiveBackground).color().name()
852                     : QStringLiteral("#FAE9EB"))); //KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NegativeBackground).color().name()));
853         mComplianceLbl->setText(de_vs
854                 ? i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
855                     "%1 communication possible.", Formatting::deVsString())
856                 : i18nc("%1 is a placeholder for the name of a compliance mode. E.g. NATO RESTRICTED compliant or VS-NfD compliant",
857                     "%1 communication not possible.", Formatting::deVsString()));
858         mComplianceLbl->setVisible(true);
859     }
860 
861     GpgME::Protocol mForcedProtocol;
862     QList<KeySelectionCombo *> mSigningCombos;
863     QList<KeySelectionCombo *> mEncCombos;
864     QList<KeySelectionCombo *> mAllCombos;
865     QScrollArea *mScrollArea;
866     QVBoxLayout *mScrollLayout;
867     QPushButton *mOkButton;
868     QVBoxLayout *mMainLay;
869     QButtonGroup *mFormatBtns;
870     QString mSender;
871     bool mSign;
872     bool mEncrypt;
873     bool mAllowMixed;
874     NewKeyApprovalDialog *q;
875     QList <QGpgME::Job *> mRunningJobs;
876     GpgME::Error mLastError;
877     QLabel *mComplianceLbl;
878     KeyResolver::Solution mAcceptedResult;
879     QString mGenerateTooltip;
880 };
881 
NewKeyApprovalDialog(bool encrypt,bool sign,const QString & sender,KeyResolver::Solution preferredSolution,KeyResolver::Solution alternativeSolution,bool allowMixed,GpgME::Protocol forcedProtocol,QWidget * parent,Qt::WindowFlags f)882 NewKeyApprovalDialog::NewKeyApprovalDialog(bool encrypt,
883                                            bool sign,
884                                            const QString &sender,
885                                            KeyResolver::Solution preferredSolution,
886                                            KeyResolver::Solution alternativeSolution,
887                                            bool allowMixed,
888                                            GpgME::Protocol forcedProtocol,
889                                            QWidget *parent,
890                                            Qt::WindowFlags f)
891     : QDialog(parent, f)
892     , d{std::make_unique<Private>(this, encrypt, sign, forcedProtocol, preferredSolution.protocol, sender, allowMixed)}
893 {
894     if (sign) {
895         d->setSigningKeys(std::move(preferredSolution.signingKeys), std::move(alternativeSolution.signingKeys));
896     }
897     if (encrypt) {
898         d->setEncryptionKeys(allowMixed ? UnknownProtocol : preferredSolution.protocol, std::move(preferredSolution.encryptionKeys),
899                              allowMixed ? UnknownProtocol : alternativeSolution.protocol, std::move(alternativeSolution.encryptionKeys));
900     }
901     d->updateWidgets();
902     d->updateOkButton();
903 
904     const auto size = sizeHint();
905     const auto desk = QApplication::desktop()->screenGeometry(this);
906     resize(QSize(desk.width() / 3, qMin(size.height(), desk.height() / 2)));
907 }
908 
909 Kleo::NewKeyApprovalDialog::~NewKeyApprovalDialog() = default;
910 
result()911 KeyResolver::Solution NewKeyApprovalDialog::result()
912 {
913     return d->mAcceptedResult;
914 }
915 
916 #include "newkeyapprovaldialog.moc"
917