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 ¶ms, 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