1 /*  view/pgpcardwiget.cpp
2 
3     This file is part of Kleopatra, the KDE keymanager
4     SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik
5     SPDX-FileContributor: Intevation GmbH
6     SPDX-FileCopyrightText: 2020 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 "pgpcardwidget.h"
13 
14 #include "openpgpkeycardwidget.h"
15 
16 #include "kleopatra_debug.h"
17 
18 #include "commands/createcsrforcardkeycommand.h"
19 #include "commands/createopenpgpkeyfromcardkeyscommand.h"
20 
21 #include "smartcard/openpgpcard.h"
22 #include "smartcard/readerstatus.h"
23 
24 #include "dialogs/gencardkeydialog.h"
25 
26 #include <Libkleo/GnuPG>
27 
28 #include <QProgressDialog>
29 #include <QThread>
30 #include <QScrollArea>
31 #include <QInputDialog>
32 #include <QFileDialog>
33 #include <QFileInfo>
34 #include <QGridLayout>
35 #include <QPushButton>
36 #include <QLabel>
37 #include <QHBoxLayout>
38 #include <QVBoxLayout>
39 
40 #include <KLocalizedString>
41 #include <KMessageBox>
42 #include <KSeparator>
43 
44 #include <Libkleo/KeyCache>
45 #include <Libkleo/Formatting>
46 
47 #include <gpgme++/data.h>
48 #include <gpgme++/context.h>
49 
50 #include <QGpgME/DataProvider>
51 
52 #include <gpgme++/gpggencardkeyinteractor.h>
53 
54 using namespace Kleo;
55 using namespace Kleo::Commands;
56 using namespace Kleo::SmartCard;
57 
58 namespace {
59 class GenKeyThread: public QThread
60 {
61     Q_OBJECT
62 
63     public:
GenKeyThread(const GenCardKeyDialog::KeyParams & params,const std::string & serial)64         explicit GenKeyThread(const GenCardKeyDialog::KeyParams &params, const std::string &serial):
65             mSerial(serial),
66             mParams(params)
67         {
68         }
69 
error()70         GpgME::Error error()
71         {
72             return mErr;
73         }
74 
bkpFile()75         std::string bkpFile()
76         {
77             return mBkpFile;
78         }
79     protected:
run()80         void run() override {
81             auto ei = new GpgME::GpgGenCardKeyInteractor(mSerial);
82             ei->setAlgo(GpgME::GpgGenCardKeyInteractor::RSA);
83             ei->setKeySize(QByteArray::fromStdString(mParams.algorithm).toInt());
84             ei->setNameUtf8(mParams.name.toStdString());
85             ei->setEmailUtf8(mParams.email.toStdString());
86             ei->setDoBackup(mParams.backup);
87 
88             const auto ctx = std::shared_ptr<GpgME::Context> (GpgME::Context::createForProtocol(GpgME::OpenPGP));
89             QGpgME::QByteArrayDataProvider dp;
90             GpgME::Data data(&dp);
91 
92             mErr = ctx->cardEdit(GpgME::Key(), std::unique_ptr<GpgME::EditInteractor> (ei), data);
93             mBkpFile = ei->backupFileName();
94         }
95 
96     private:
97         GpgME::Error mErr;
98         std::string mSerial;
99         GenCardKeyDialog::KeyParams mParams;
100 
101         std::string mBkpFile;
102 };
103 
104 } // Namespace
105 
PGPCardWidget(QWidget * parent)106 PGPCardWidget::PGPCardWidget(QWidget *parent):
107     QWidget(parent),
108     mSerialNumber(new QLabel(this)),
109     mCardHolderLabel(new QLabel(this)),
110     mVersionLabel(new QLabel(this)),
111     mUrlLabel(new QLabel(this)),
112     mCardIsEmpty(false)
113 {
114     // Set up the scroll area
115     auto myLayout = new QVBoxLayout(this);
116     myLayout->setContentsMargins(0, 0, 0, 0);
117 
118     auto area = new QScrollArea;
119     area->setFrameShape(QFrame::NoFrame);
120     area->setWidgetResizable(true);
121     myLayout->addWidget(area);
122 
123     auto areaWidget = new QWidget;
124     area->setWidget(areaWidget);
125 
126     auto areaVLay = new QVBoxLayout(areaWidget);
127 
128     auto cardInfoGrid = new QGridLayout;
129     {
130         int row = 0;
131 
132         // Version and Serialnumber
133         cardInfoGrid->addWidget(mVersionLabel, row, 0, 1, 2);
134         mVersionLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
135         row++;
136 
137         cardInfoGrid->addWidget(new QLabel(i18n("Serial number:")), row, 0);
138         cardInfoGrid->addWidget(mSerialNumber, row, 1);
139         mSerialNumber->setTextInteractionFlags(Qt::TextBrowserInteraction);
140         row++;
141 
142         // Cardholder Row
143         cardInfoGrid->addWidget(new QLabel(i18nc("The owner of a smartcard. GnuPG refers to this as cardholder.",
144                                                  "Cardholder:")), row, 0);
145         cardInfoGrid->addWidget(mCardHolderLabel, row, 1);
146         mCardHolderLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
147         {
148             auto button = new QPushButton;
149             button->setIcon(QIcon::fromTheme(QStringLiteral("cell_edit")));
150             button->setToolTip(i18n("Change"));
151             cardInfoGrid->addWidget(button, row, 2);
152             connect(button, &QPushButton::clicked, this, &PGPCardWidget::changeNameRequested);
153         }
154         row++;
155 
156         // URL Row
157         cardInfoGrid->addWidget(new QLabel(i18nc("The URL under which a public key that "
158                                                  "corresponds to a smartcard can be downloaded",
159                                                  "Pubkey URL:")), row, 0);
160         cardInfoGrid->addWidget(mUrlLabel, row, 1);
161         mUrlLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
162         {
163             auto button = new QPushButton;
164             button->setIcon(QIcon::fromTheme(QStringLiteral("cell_edit")));
165             button->setToolTip(i18n("Change"));
166             cardInfoGrid->addWidget(button, row, 2);
167             connect(button, &QPushButton::clicked, this, &PGPCardWidget::changeUrlRequested);
168         }
169 
170         cardInfoGrid->setColumnStretch(cardInfoGrid->columnCount(), 1);
171     }
172     areaVLay->addLayout(cardInfoGrid);
173 
174     areaVLay->addWidget(new KSeparator(Qt::Horizontal));
175 
176     // The keys
177     areaVLay->addWidget(new QLabel(QStringLiteral("<b>%1</b>").arg(i18n("Keys:"))));
178 
179     mKeysWidget = new OpenPGPKeyCardWidget{this};
180     areaVLay->addWidget(mKeysWidget);
181     connect(mKeysWidget, &OpenPGPKeyCardWidget::createCSRRequested, this, &PGPCardWidget::createCSR);
182 
183     areaVLay->addWidget(new KSeparator(Qt::Horizontal));
184 
185     areaVLay->addWidget(new QLabel(QStringLiteral("<b>%1</b>").arg(i18n("Actions:"))));
186 
187     auto actionLayout = new QHBoxLayout;
188 
189     {
190         auto generateButton = new QPushButton(i18n("Generate New Keys"));
191         generateButton->setToolTip(i18n("Create a new primary key and generate subkeys on the card."));
192         actionLayout->addWidget(generateButton);
193         connect(generateButton, &QPushButton::clicked, this, &PGPCardWidget::genkeyRequested);
194     }
195     {
196         auto pinButton = new QPushButton(i18n("Change PIN"));
197         pinButton->setToolTip(i18n("Change the PIN required for using the keys on the smartcard."));
198         actionLayout->addWidget(pinButton);
199         connect(pinButton, &QPushButton::clicked, this, [this] () { doChangePin(OpenPGPCard::pinKeyRef()); });
200     }
201     {
202         auto unblockButton = new QPushButton(i18n("Unblock Card"));
203         unblockButton->setToolTip(i18n("Unblock the smartcard and set a new PIN."));
204         actionLayout->addWidget(unblockButton);
205         connect(unblockButton, &QPushButton::clicked, this, [this] () { doChangePin(OpenPGPCard::resetCodeKeyRef()); });
206     }
207     {
208         auto pukButton = new QPushButton(i18n("Change Admin PIN"));
209         pukButton->setToolTip(i18n("Change the PIN required for administrative operations."));
210         actionLayout->addWidget(pukButton);
211         connect(pukButton, &QPushButton::clicked, this, [this] () { doChangePin(OpenPGPCard::adminPinKeyRef()); });
212     }
213     {
214         auto resetCodeButton = new QPushButton(i18n("Change Reset Code"));
215         resetCodeButton->setToolTip(i18n("Change the PIN required to unblock the smartcard and set a new PIN."));
216         actionLayout->addWidget(resetCodeButton);
217         connect(resetCodeButton, &QPushButton::clicked,
218                 this, [this] () { doChangePin(OpenPGPCard::resetCodeKeyRef(), ChangePinCommand::ResetMode); });
219     }
220 
221     if (CreateOpenPGPKeyFromCardKeysCommand::isSupported()) {
222         mKeyForCardKeysButton = new QPushButton(this);
223         mKeyForCardKeysButton->setText(i18n("Create OpenPGP Key"));
224         mKeyForCardKeysButton->setToolTip(i18n("Create an OpenPGP key for the keys stored on the card."));
225         actionLayout->addWidget(mKeyForCardKeysButton);
226         connect(mKeyForCardKeysButton, &QPushButton::clicked, this, &PGPCardWidget::createKeyFromCardKeys);
227     }
228 
229     actionLayout->addStretch(-1);
230     areaVLay->addLayout(actionLayout);
231 
232     areaVLay->addStretch(1);
233 }
234 
setCard(const OpenPGPCard * card)235 void PGPCardWidget::setCard(const OpenPGPCard *card)
236 {
237     const QString version = card->displayAppVersion();
238 
239     mIs21 = card->appVersion() >= 0x0201;
240     const QString manufacturer = QString::fromStdString(card->manufacturer());
241     const bool manufacturerIsUnknown = manufacturer.isEmpty() || manufacturer == QLatin1String("unknown");
242     mVersionLabel->setText(manufacturerIsUnknown ?
243         i18nc("Placeholder is a version number", "Unknown OpenPGP v%1 card", version) :
244         i18nc("First placeholder is manufacturer, second placeholder is a version number",
245               "%1 OpenPGP v%2 card", manufacturer, version));
246     mSerialNumber->setText(card->displaySerialNumber());
247     mRealSerial = card->serialNumber();
248 
249     const auto holder = card->cardHolder();
250     const auto url = QString::fromStdString(card->pubkeyUrl());
251     mCardHolderLabel->setText(holder.isEmpty() ? i18n("not set") : holder);
252     mUrl = url;
253     mUrlLabel->setText(url.isEmpty() ? i18n("not set") :
254                        QStringLiteral("<a href=\"%1\">%1</a>").arg(url.toHtmlEscaped()));
255     mUrlLabel->setOpenExternalLinks(true);
256 
257     mKeysWidget->update(card);
258 
259     mCardIsEmpty = card->keyFingerprint(OpenPGPCard::pgpSigKeyRef()).empty()
260         && card->keyFingerprint(OpenPGPCard::pgpEncKeyRef()).empty()
261         && card->keyFingerprint(OpenPGPCard::pgpAuthKeyRef()).empty();
262 
263     if (mKeyForCardKeysButton) {
264         mKeyForCardKeysButton->setEnabled(card->hasSigningKey() && card->hasEncryptionKey());
265     }
266 }
267 
doChangePin(const std::string & keyRef,ChangePinCommand::ChangePinMode mode)268 void PGPCardWidget::doChangePin(const std::string &keyRef, ChangePinCommand::ChangePinMode mode)
269 {
270     auto cmd = new ChangePinCommand(mRealSerial, OpenPGPCard::AppName, this);
271     this->setEnabled(false);
272     connect(cmd, &ChangePinCommand::finished,
273             this, [this]() {
274                 this->setEnabled(true);
275             });
276     cmd->setKeyRef(keyRef);
277     cmd->setMode(mode);
278     cmd->start();
279 }
280 
doGenKey(GenCardKeyDialog * dlg)281 void PGPCardWidget::doGenKey(GenCardKeyDialog *dlg)
282 {
283     const GpgME::Error err = ReaderStatus::switchCardAndApp(mRealSerial, OpenPGPCard::AppName);
284     if (err) {
285         return;
286     }
287 
288     const auto params = dlg->getKeyParams();
289 
290     auto progress = new QProgressDialog(this, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::Dialog);
291     progress->setAutoClose(true);
292     progress->setMinimumDuration(0);
293     progress->setMaximum(0);
294     progress->setMinimum(0);
295     progress->setModal(true);
296     progress->setCancelButton(nullptr);
297     progress->setWindowTitle(i18nc("@title:window", "Generating Keys"));
298     progress->setLabel(new QLabel(i18n("This may take several minutes...")));
299     auto workerThread = new GenKeyThread(params, mRealSerial);
300     connect(workerThread, &QThread::finished, this, [this, workerThread, progress] {
301             progress->accept();
302             progress->deleteLater();
303             genKeyDone(workerThread->error(), workerThread->bkpFile());
304             delete workerThread;
305         });
306     workerThread->start();
307     progress->exec();
308 }
309 
genKeyDone(const GpgME::Error & err,const std::string & backup)310 void PGPCardWidget::genKeyDone(const GpgME::Error &err, const std::string &backup)
311 {
312     if (err) {
313         KMessageBox::error(this, i18nc("@info",
314                            "Failed to generate new key: %1", QString::fromLatin1(err.asString())),
315                            i18nc("@title", "Error"));
316         return;
317     }
318     if (err.isCanceled()) {
319         return;
320     }
321     if (!backup.empty()) {
322         const auto bkpFile = QString::fromStdString(backup);
323         QFileInfo fi(bkpFile);
324         const auto target = QFileDialog::getSaveFileName(this, i18n("Save backup of encryption key"),
325                                                          fi.fileName(),
326                                                          QStringLiteral("%1 (*.gpg)").arg(i18n("Backup Key")));
327         if (!target.isEmpty() && !QFile::copy(bkpFile, target)) {
328             KMessageBox::error(this, i18nc("@info",
329                                "Failed to move backup. The backup key is still stored under: %1", bkpFile),
330                                i18nc("@title", "Error"));
331         } else if (!target.isEmpty()) {
332             QFile::remove(bkpFile);
333         }
334     }
335 
336     KMessageBox::information(this, i18nc("@info",
337                              "Successfully generated a new key for this card."),
338                              i18nc("@title", "Success"));
339     ReaderStatus::mutableInstance()->updateStatus();
340 }
341 
genkeyRequested()342 void PGPCardWidget::genkeyRequested()
343 {
344     if (!mCardIsEmpty) {
345         auto ret = KMessageBox::warningContinueCancel(this,
346                 i18n("The existing keys on this card will be <b>deleted</b> "
347                      "and replaced by new keys.") + QStringLiteral("<br/><br/>") +
348                 i18n("It will no longer be possible to decrypt past communication "
349                      "encrypted for the existing key."),
350                 i18n("Secret Key Deletion"),
351                 KStandardGuiItem::guiItem(KStandardGuiItem::Delete),
352                 KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous);
353 
354         if (ret != KMessageBox::Continue) {
355             return;
356         }
357     }
358 
359     auto dlg = new GenCardKeyDialog(GenCardKeyDialog::AllKeyAttributes, this);
360     std::vector<std::pair<std::string, QString>> algos = {
361         { "1024", QStringLiteral("RSA 1024") },
362         { "2048", QStringLiteral("RSA 2048") },
363         { "3072", QStringLiteral("RSA 3072") }
364     };
365     // There is probably a better way to check for capabilities
366     if (mIs21) {
367         algos.push_back({"4096", QStringLiteral("RSA 4096")});
368     }
369     dlg->setSupportedAlgorithms(algos, "2048");
370     connect(dlg, &QDialog::accepted, this, [this, dlg] () {
371             doGenKey(dlg);
372             dlg->deleteLater();
373         });
374     dlg->setModal(true);
375     dlg->show();
376 }
377 
changeNameRequested()378 void PGPCardWidget::changeNameRequested()
379 {
380     QString text = mCardHolderLabel->text();
381     while (true) {
382         bool ok = false;
383         text = QInputDialog::getText(this, i18n("Change cardholder"),
384                                      i18n("New name:"), QLineEdit::Normal,
385                                      text, &ok, Qt::WindowFlags(),
386                                      Qt::ImhLatinOnly);
387         if (!ok) {
388             return;
389         }
390         // Some additional restrictions imposed by gnupg
391         if (text.contains(QLatin1Char('<'))) {
392             KMessageBox::error(this, i18nc("@info",
393                                "The \"<\" character may not be used."),
394                                i18nc("@title", "Error"));
395             continue;
396         }
397         if (text.contains(QLatin1String("  "))) {
398             KMessageBox::error(this, i18nc("@info",
399                                "Double spaces are not allowed"),
400                                i18nc("@title", "Error"));
401             continue;
402         }
403         if (text.size() > 38) {
404             KMessageBox::error(this, i18nc("@info",
405                                "The size of the name may not exceed 38 characters."),
406                                i18nc("@title", "Error"));
407         }
408         break;
409     }
410     auto parts = text.split(QLatin1Char(' '));
411     const auto lastName = parts.takeLast();
412     const QString formatted = lastName + QStringLiteral("<<") + parts.join(QLatin1Char('<'));
413 
414     const auto pgpCard = ReaderStatus::instance()->getCard<OpenPGPCard>(mRealSerial);
415     if (!pgpCard) {
416         KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial)));
417         return;
418     }
419 
420     const QByteArray command = QByteArrayLiteral("SCD SETATTR DISP-NAME ") + formatted.toUtf8();
421     ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, command, this, "changeNameResult");
422 }
423 
changeNameResult(const GpgME::Error & err)424 void PGPCardWidget::changeNameResult(const GpgME::Error &err)
425 {
426     if (err) {
427         KMessageBox::error(this, i18nc("@info",
428                            "Name change failed: %1", QString::fromLatin1(err.asString())),
429                            i18nc("@title", "Error"));
430         return;
431     }
432     if (!err.isCanceled()) {
433         KMessageBox::information(this, i18nc("@info",
434                     "Name successfully changed."),
435                 i18nc("@title", "Success"));
436         ReaderStatus::mutableInstance()->updateStatus();
437     }
438 }
439 
changeUrlRequested()440 void PGPCardWidget::changeUrlRequested()
441 {
442     QString text = mUrl;
443     while (true) {
444         bool ok = false;
445         text = QInputDialog::getText(this, i18n("Change the URL where the pubkey can be found"),
446                                      i18n("New pubkey URL:"), QLineEdit::Normal,
447                                      text, &ok, Qt::WindowFlags(),
448                                      Qt::ImhLatinOnly);
449         if (!ok) {
450             return;
451         }
452         // Some additional restrictions imposed by gnupg
453         if (text.size() > 254) {
454             KMessageBox::error(this, i18nc("@info",
455                                "The size of the URL may not exceed 254 characters."),
456                                i18nc("@title", "Error"));
457         }
458         break;
459     }
460 
461     const auto pgpCard = ReaderStatus::instance()->getCard<OpenPGPCard>(mRealSerial);
462     if (!pgpCard) {
463         KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial)));
464         return;
465     }
466 
467     const QByteArray command = QByteArrayLiteral("SCD SETATTR PUBKEY-URL ") + text.toUtf8();
468     ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, command, this, "changeUrlResult");
469 }
470 
changeUrlResult(const GpgME::Error & err)471 void PGPCardWidget::changeUrlResult(const GpgME::Error &err)
472 {
473     if (err) {
474         KMessageBox::error(this, i18nc("@info",
475                            "URL change failed: %1", QString::fromLatin1(err.asString())),
476                            i18nc("@title", "Error"));
477         return;
478     }
479     if (!err.isCanceled()) {
480         KMessageBox::information(this, i18nc("@info",
481                     "URL successfully changed."),
482                 i18nc("@title", "Success"));
483         ReaderStatus::mutableInstance()->updateStatus();
484     }
485 }
486 
createKeyFromCardKeys()487 void PGPCardWidget::createKeyFromCardKeys()
488 {
489     auto cmd = new CreateOpenPGPKeyFromCardKeysCommand(mRealSerial, OpenPGPCard::AppName, this);
490     this->setEnabled(false);
491     connect(cmd, &CreateOpenPGPKeyFromCardKeysCommand::finished,
492             this, [this]() {
493                 this->setEnabled(true);
494             });
495     cmd->start();
496 }
497 
createCSR(const std::string & keyref)498 void PGPCardWidget::createCSR(const std::string &keyref)
499 {
500     auto cmd = new CreateCSRForCardKeyCommand(keyref, mRealSerial, OpenPGPCard::AppName, this);
501     this->setEnabled(false);
502     connect(cmd, &CreateCSRForCardKeyCommand::finished,
503             this, [this]() {
504                 this->setEnabled(true);
505             });
506     cmd->start();
507 }
508 
509 #include "pgpcardwidget.moc"
510