File indexing completed on 2024-06-16 04:56:16
0001 /* view/pgpcardwiget.cpp 0002 0003 This file is part of Kleopatra, the KDE keymanager 0004 SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik 0005 SPDX-FileContributor: Intevation GmbH 0006 SPDX-FileCopyrightText: 2020, 2022 g10 Code GmbH 0007 SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> 0008 0009 SPDX-License-Identifier: GPL-2.0-or-later 0010 */ 0011 0012 #include <config-kleopatra.h> 0013 0014 #include "pgpcardwidget.h" 0015 0016 #include "openpgpkeycardwidget.h" 0017 0018 #include "kleopatra_debug.h" 0019 0020 #include "commands/createcsrforcardkeycommand.h" 0021 #include "commands/createopenpgpkeyfromcardkeyscommand.h" 0022 #include "commands/openpgpgeneratecardkeycommand.h" 0023 0024 #include "smartcard/algorithminfo.h" 0025 #include "smartcard/openpgpcard.h" 0026 #include "smartcard/readerstatus.h" 0027 #include "smartcard/utils.h" 0028 0029 #include "dialogs/gencardkeydialog.h" 0030 0031 #include <Libkleo/Compliance> 0032 #include <Libkleo/GnuPG> 0033 0034 #include <QFileDialog> 0035 #include <QFileInfo> 0036 #include <QGridLayout> 0037 #include <QHBoxLayout> 0038 #include <QInputDialog> 0039 #include <QLabel> 0040 #include <QProgressDialog> 0041 #include <QPushButton> 0042 #include <QScrollArea> 0043 #include <QThread> 0044 #include <QVBoxLayout> 0045 0046 #include <KLocalizedString> 0047 #include <KMessageBox> 0048 #include <KSeparator> 0049 0050 #include <Libkleo/Formatting> 0051 #include <Libkleo/KeyCache> 0052 0053 #include <gpgme++/context.h> 0054 #include <gpgme++/data.h> 0055 0056 #include <QGpgME/DataProvider> 0057 0058 #include <gpgme++/gpggencardkeyinteractor.h> 0059 0060 using namespace Kleo; 0061 using namespace Kleo::Commands; 0062 using namespace Kleo::SmartCard; 0063 0064 namespace 0065 { 0066 class GenKeyThread : public QThread 0067 { 0068 Q_OBJECT 0069 0070 public: 0071 explicit GenKeyThread(const GenCardKeyDialog::KeyParams ¶ms, const std::string &serial) 0072 : mSerial(serial) 0073 , mParams(params) 0074 { 0075 } 0076 0077 GpgME::Error error() 0078 { 0079 return mErr; 0080 } 0081 0082 std::string bkpFile() 0083 { 0084 return mBkpFile; 0085 } 0086 0087 protected: 0088 void run() override 0089 { 0090 // the index of the curves in this list has to match the enum values 0091 // minus 1 of GpgGenCardKeyInteractor::Curve 0092 static const std::vector<std::string> curves = { 0093 "curve25519", 0094 "curve448", 0095 "nistp256", 0096 "nistp384", 0097 "nistp521", 0098 "brainpoolP256r1", 0099 "brainpoolP384r1", 0100 "brainpoolP512r1", 0101 "secp256k1", // keep it, even if we don't support it in Kleopatra 0102 }; 0103 0104 auto ei = std::make_unique<GpgME::GpgGenCardKeyInteractor>(mSerial); 0105 if (mParams.algorithm.starts_with("rsa")) { 0106 ei->setAlgo(GpgME::GpgGenCardKeyInteractor::RSA); 0107 ei->setKeySize(QByteArray::fromStdString(mParams.algorithm.substr(3)).toInt()); 0108 } else { 0109 ei->setAlgo(GpgME::GpgGenCardKeyInteractor::ECC); 0110 const auto curveIt = std::find(curves.cbegin(), curves.cend(), mParams.algorithm); 0111 if (curveIt != curves.end()) { 0112 ei->setCurve(static_cast<GpgME::GpgGenCardKeyInteractor::Curve>(curveIt - curves.cbegin() + 1)); 0113 } else { 0114 qCWarning(KLEOPATRA_LOG) << this << __func__ << "Invalid curve name:" << mParams.algorithm; 0115 mErr = GpgME::Error::fromCode(GPG_ERR_INV_VALUE); 0116 return; 0117 } 0118 } 0119 ei->setNameUtf8(mParams.name.toStdString()); 0120 ei->setEmailUtf8(mParams.email.toStdString()); 0121 ei->setDoBackup(mParams.backup); 0122 0123 const auto ctx = std::shared_ptr<GpgME::Context>(GpgME::Context::createForProtocol(GpgME::OpenPGP)); 0124 ctx->setFlag("extended-edit", "1"); // we want to be able to select all curves 0125 QGpgME::QByteArrayDataProvider dp; 0126 GpgME::Data data(&dp); 0127 0128 mErr = ctx->cardEdit(GpgME::Key(), std::move(ei), data); 0129 mBkpFile = static_cast<GpgME::GpgGenCardKeyInteractor *>(ctx->lastCardEditInteractor())->backupFileName(); 0130 } 0131 0132 private: 0133 GpgME::Error mErr; 0134 std::string mSerial; 0135 GenCardKeyDialog::KeyParams mParams; 0136 0137 std::string mBkpFile; 0138 }; 0139 0140 } // Namespace 0141 0142 PGPCardWidget::PGPCardWidget(QWidget *parent) 0143 : QWidget(parent) 0144 , mSerialNumber(new QLabel(this)) 0145 , mCardHolderLabel(new QLabel(this)) 0146 , mVersionLabel(new QLabel(this)) 0147 , mUrlLabel(new QLabel(this)) 0148 , mCardIsEmpty(false) 0149 { 0150 // Set up the scroll area 0151 auto myLayout = new QVBoxLayout(this); 0152 myLayout->setContentsMargins(0, 0, 0, 0); 0153 0154 auto area = new QScrollArea; 0155 area->setFrameShape(QFrame::NoFrame); 0156 area->setWidgetResizable(true); 0157 myLayout->addWidget(area); 0158 0159 auto areaWidget = new QWidget; 0160 area->setWidget(areaWidget); 0161 0162 auto areaVLay = new QVBoxLayout(areaWidget); 0163 0164 auto cardInfoGrid = new QGridLayout; 0165 { 0166 int row = 0; 0167 0168 // Version and Serialnumber 0169 cardInfoGrid->addWidget(mVersionLabel, row, 0, 1, 2); 0170 mVersionLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); 0171 row++; 0172 0173 cardInfoGrid->addWidget(new QLabel(i18n("Serial number:")), row, 0); 0174 cardInfoGrid->addWidget(mSerialNumber, row, 1); 0175 mSerialNumber->setTextInteractionFlags(Qt::TextBrowserInteraction); 0176 row++; 0177 0178 // Cardholder Row 0179 cardInfoGrid->addWidget(new QLabel(i18nc("The owner of a smartcard. GnuPG refers to this as cardholder.", "Cardholder:")), row, 0); 0180 cardInfoGrid->addWidget(mCardHolderLabel, row, 1); 0181 mCardHolderLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); 0182 { 0183 auto button = new QPushButton; 0184 button->setIcon(QIcon::fromTheme(QStringLiteral("cell_edit"))); 0185 button->setAccessibleName(i18nc("@action:button", "Edit")); 0186 button->setToolTip(i18n("Change")); 0187 cardInfoGrid->addWidget(button, row, 2); 0188 connect(button, &QPushButton::clicked, this, &PGPCardWidget::changeNameRequested); 0189 } 0190 row++; 0191 0192 // URL Row 0193 cardInfoGrid->addWidget(new QLabel(i18nc("The URL under which a public key that " 0194 "corresponds to a smartcard can be downloaded", 0195 "Pubkey URL:")), 0196 row, 0197 0); 0198 cardInfoGrid->addWidget(mUrlLabel, row, 1); 0199 mUrlLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); 0200 { 0201 auto button = new QPushButton; 0202 button->setIcon(QIcon::fromTheme(QStringLiteral("cell_edit"))); 0203 button->setAccessibleName(i18nc("@action:button", "Edit")); 0204 button->setToolTip(i18n("Change")); 0205 cardInfoGrid->addWidget(button, row, 2); 0206 connect(button, &QPushButton::clicked, this, &PGPCardWidget::changeUrlRequested); 0207 } 0208 0209 cardInfoGrid->setColumnStretch(cardInfoGrid->columnCount(), 1); 0210 } 0211 areaVLay->addLayout(cardInfoGrid); 0212 0213 areaVLay->addWidget(new KSeparator(Qt::Horizontal)); 0214 0215 // The keys 0216 areaVLay->addWidget(new QLabel(QStringLiteral("<b>%1</b>").arg(i18n("Keys:")))); 0217 0218 mKeysWidget = new OpenPGPKeyCardWidget{this}; 0219 areaVLay->addWidget(mKeysWidget); 0220 connect(mKeysWidget, &OpenPGPKeyCardWidget::createCSRRequested, this, &PGPCardWidget::createCSR); 0221 connect(mKeysWidget, &OpenPGPKeyCardWidget::generateKeyRequested, this, &PGPCardWidget::generateKey); 0222 0223 areaVLay->addWidget(new KSeparator(Qt::Horizontal)); 0224 0225 areaVLay->addWidget(new QLabel(QStringLiteral("<b>%1</b>").arg(i18n("Actions:")))); 0226 0227 auto actionLayout = new QHBoxLayout; 0228 0229 { 0230 auto generateButton = new QPushButton(i18n("Generate New Keys")); 0231 generateButton->setToolTip(i18n("Create a new primary key and generate subkeys on the card.")); 0232 actionLayout->addWidget(generateButton); 0233 connect(generateButton, &QPushButton::clicked, this, &PGPCardWidget::genkeyRequested); 0234 } 0235 { 0236 auto pinButton = new QPushButton(i18n("Change PIN")); 0237 pinButton->setToolTip(i18n("Change the PIN required for using the keys on the smartcard.")); 0238 actionLayout->addWidget(pinButton); 0239 connect(pinButton, &QPushButton::clicked, this, [this]() { 0240 doChangePin(OpenPGPCard::pinKeyRef()); 0241 }); 0242 } 0243 { 0244 auto unblockButton = new QPushButton(i18n("Unblock Card")); 0245 unblockButton->setToolTip(i18n("Unblock the smartcard and set a new PIN.")); 0246 actionLayout->addWidget(unblockButton); 0247 connect(unblockButton, &QPushButton::clicked, this, [this]() { 0248 doChangePin(OpenPGPCard::resetCodeKeyRef()); 0249 }); 0250 } 0251 { 0252 auto pukButton = new QPushButton(i18n("Change Admin PIN")); 0253 pukButton->setToolTip(i18n("Change the PIN required for administrative operations.")); 0254 actionLayout->addWidget(pukButton); 0255 connect(pukButton, &QPushButton::clicked, this, [this]() { 0256 doChangePin(OpenPGPCard::adminPinKeyRef()); 0257 }); 0258 } 0259 { 0260 auto resetCodeButton = new QPushButton(i18n("Change Reset Code")); 0261 resetCodeButton->setToolTip(i18n("Change the PIN required to unblock the smartcard and set a new PIN.")); 0262 actionLayout->addWidget(resetCodeButton); 0263 connect(resetCodeButton, &QPushButton::clicked, this, [this]() { 0264 doChangePin(OpenPGPCard::resetCodeKeyRef(), ChangePinCommand::ResetMode); 0265 }); 0266 } 0267 0268 if (CreateOpenPGPKeyFromCardKeysCommand::isSupported()) { 0269 mKeyForCardKeysButton = new QPushButton(this); 0270 mKeyForCardKeysButton->setText(i18n("Create OpenPGP Key")); 0271 mKeyForCardKeysButton->setToolTip(i18n("Create an OpenPGP key for the keys stored on the card.")); 0272 actionLayout->addWidget(mKeyForCardKeysButton); 0273 connect(mKeyForCardKeysButton, &QPushButton::clicked, this, &PGPCardWidget::createKeyFromCardKeys); 0274 } 0275 0276 actionLayout->addStretch(-1); 0277 areaVLay->addLayout(actionLayout); 0278 0279 areaVLay->addStretch(1); 0280 } 0281 0282 void PGPCardWidget::setCard(const OpenPGPCard *card) 0283 { 0284 const QString version = card->displayAppVersion(); 0285 0286 mIs21 = card->appVersion() >= 0x0201; 0287 const QString manufacturer = QString::fromStdString(card->manufacturer()); 0288 const bool manufacturerIsUnknown = manufacturer.isEmpty() || manufacturer == QLatin1StringView("unknown"); 0289 mVersionLabel->setText( 0290 manufacturerIsUnknown 0291 ? i18nc("Placeholder is a version number", "Unknown OpenPGP v%1 card", version) 0292 : i18nc("First placeholder is manufacturer, second placeholder is a version number", "%1 OpenPGP v%2 card", manufacturer, version)); 0293 mSerialNumber->setText(card->displaySerialNumber()); 0294 mRealSerial = card->serialNumber(); 0295 0296 const auto holder = card->cardHolder(); 0297 const auto url = QString::fromStdString(card->pubkeyUrl()); 0298 mCardHolderLabel->setText(holder.isEmpty() ? i18n("not set") : holder); 0299 mUrl = url; 0300 mUrlLabel->setText(url.isEmpty() ? i18n("not set") : QStringLiteral("<a href=\"%1\">%1</a>").arg(url.toHtmlEscaped())); 0301 mUrlLabel->setOpenExternalLinks(true); 0302 0303 mKeysWidget->update(card); 0304 0305 mCardIsEmpty = card->keyFingerprint(OpenPGPCard::pgpSigKeyRef()).empty() && card->keyFingerprint(OpenPGPCard::pgpEncKeyRef()).empty() 0306 && card->keyFingerprint(OpenPGPCard::pgpAuthKeyRef()).empty(); 0307 0308 if (mKeyForCardKeysButton) { 0309 mKeyForCardKeysButton->setEnabled(card->hasSigningKey() // 0310 && card->hasEncryptionKey() // 0311 && DeVSCompliance::algorithmIsCompliant(card->keyInfo(card->signingKeyRef()).algorithm) 0312 && DeVSCompliance::algorithmIsCompliant(card->keyInfo(card->encryptionKeyRef()).algorithm)); 0313 } 0314 } 0315 0316 void PGPCardWidget::doChangePin(const std::string &keyRef, ChangePinCommand::ChangePinMode mode) 0317 { 0318 auto cmd = new ChangePinCommand(mRealSerial, OpenPGPCard::AppName, this); 0319 this->setEnabled(false); 0320 connect(cmd, &ChangePinCommand::finished, this, [this]() { 0321 this->setEnabled(true); 0322 }); 0323 cmd->setKeyRef(keyRef); 0324 cmd->setMode(mode); 0325 cmd->start(); 0326 } 0327 0328 void PGPCardWidget::doGenKey(GenCardKeyDialog *dlg) 0329 { 0330 const GpgME::Error err = ReaderStatus::switchCardAndApp(mRealSerial, OpenPGPCard::AppName); 0331 if (err) { 0332 return; 0333 } 0334 0335 const auto params = dlg->getKeyParams(); 0336 0337 auto progress = new QProgressDialog(this, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::Dialog); 0338 progress->setAutoClose(true); 0339 progress->setMinimumDuration(0); 0340 progress->setMaximum(0); 0341 progress->setMinimum(0); 0342 progress->setModal(true); 0343 progress->setCancelButton(nullptr); 0344 progress->setWindowTitle(i18nc("@title:window", "Generating Keys")); 0345 progress->setLabel(new QLabel(i18n("This may take several minutes..."))); 0346 auto workerThread = new GenKeyThread(params, mRealSerial); 0347 connect(workerThread, &QThread::finished, this, [this, workerThread, progress] { 0348 progress->accept(); 0349 progress->deleteLater(); 0350 genKeyDone(workerThread->error(), workerThread->bkpFile()); 0351 delete workerThread; 0352 }); 0353 workerThread->start(); 0354 progress->exec(); 0355 } 0356 0357 void PGPCardWidget::genKeyDone(const GpgME::Error &err, const std::string &backup) 0358 { 0359 if (err) { 0360 KMessageBox::error(this, i18nc("@info", "Failed to generate new key: %1", Formatting::errorAsString(err))); 0361 return; 0362 } 0363 if (err.isCanceled()) { 0364 return; 0365 } 0366 if (!backup.empty()) { 0367 const auto bkpFile = QString::fromStdString(backup); 0368 QFileInfo fi(bkpFile); 0369 const auto target = 0370 QFileDialog::getSaveFileName(this, i18n("Save backup of encryption key"), fi.fileName(), QStringLiteral("%1 (*.gpg)").arg(i18n("Backup Key"))); 0371 if (!target.isEmpty() && !QFile::copy(bkpFile, target)) { 0372 KMessageBox::error(this, i18nc("@info", "Failed to move backup. The backup key is still stored under: %1", bkpFile)); 0373 } else if (!target.isEmpty()) { 0374 QFile::remove(bkpFile); 0375 } 0376 } 0377 0378 KMessageBox::information(this, i18nc("@info", "Successfully generated a new key for this card."), i18nc("@title", "Success")); 0379 ReaderStatus::mutableInstance()->updateStatus(); 0380 } 0381 0382 void PGPCardWidget::genkeyRequested() 0383 { 0384 const auto pgpCard = ReaderStatus::instance()->getCard<OpenPGPCard>(mRealSerial); 0385 if (!pgpCard) { 0386 KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial))); 0387 return; 0388 } 0389 0390 if (!mCardIsEmpty) { 0391 auto ret = KMessageBox::warningContinueCancel(this, 0392 i18n("The existing keys on this card will be <b>deleted</b> " 0393 "and replaced by new keys.") 0394 + QStringLiteral("<br/><br/>") 0395 + i18n("It will no longer be possible to decrypt past communication " 0396 "encrypted for the existing key."), 0397 i18n("Secret Key Deletion"), 0398 KStandardGuiItem::guiItem(KStandardGuiItem::Delete), 0399 KStandardGuiItem::cancel(), 0400 QString(), 0401 KMessageBox::Notify | KMessageBox::Dangerous); 0402 0403 if (ret != KMessageBox::Continue) { 0404 return; 0405 } 0406 } 0407 0408 auto dlg = new GenCardKeyDialog(GenCardKeyDialog::AllKeyAttributes, this); 0409 const auto allowedAlgos = getAllowedAlgorithms(pgpCard->supportedAlgorithms()); 0410 if (allowedAlgos.empty()) { 0411 KMessageBox::error(this, i18nc("@info", "You cannot generate keys on this smart card because it doesn't support any of the compliant algorithms.")); 0412 return; 0413 } 0414 dlg->setSupportedAlgorithms(allowedAlgos, getPreferredAlgorithm(allowedAlgos)); 0415 connect(dlg, &QDialog::accepted, this, [this, dlg]() { 0416 doGenKey(dlg); 0417 dlg->deleteLater(); 0418 }); 0419 dlg->setModal(true); 0420 dlg->show(); 0421 } 0422 0423 void PGPCardWidget::changeNameRequested() 0424 { 0425 QString text = mCardHolderLabel->text(); 0426 while (true) { 0427 bool ok = false; 0428 text = QInputDialog::getText(this, i18n("Change cardholder"), i18n("New name:"), QLineEdit::Normal, text, &ok, Qt::WindowFlags(), Qt::ImhLatinOnly); 0429 if (!ok) { 0430 return; 0431 } 0432 // Some additional restrictions imposed by gnupg 0433 if (text.contains(QLatin1Char('<'))) { 0434 KMessageBox::error(this, i18nc("@info", "The \"<\" character may not be used.")); 0435 continue; 0436 } 0437 if (text.contains(QLatin1StringView(" "))) { 0438 KMessageBox::error(this, i18nc("@info", "Double spaces are not allowed")); 0439 continue; 0440 } 0441 if (text.size() > 38) { 0442 KMessageBox::error(this, i18nc("@info", "The size of the name may not exceed 38 characters.")); 0443 } 0444 break; 0445 } 0446 auto parts = text.split(QLatin1Char(' ')); 0447 const auto lastName = parts.takeLast(); 0448 const QString formatted = lastName + QStringLiteral("<<") + parts.join(QLatin1Char('<')); 0449 0450 const auto pgpCard = ReaderStatus::instance()->getCard<OpenPGPCard>(mRealSerial); 0451 if (!pgpCard) { 0452 KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial))); 0453 return; 0454 } 0455 0456 const QByteArray command = QByteArrayLiteral("SCD SETATTR DISP-NAME ") + formatted.toUtf8(); 0457 ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, command, this, [this](const GpgME::Error &err) { 0458 changeNameResult(err); 0459 }); 0460 } 0461 0462 void PGPCardWidget::changeNameResult(const GpgME::Error &err) 0463 { 0464 if (err) { 0465 KMessageBox::error(this, i18nc("@info", "Name change failed: %1", Formatting::errorAsString(err))); 0466 return; 0467 } 0468 if (!err.isCanceled()) { 0469 KMessageBox::information(this, i18nc("@info", "Name successfully changed."), i18nc("@title", "Success")); 0470 ReaderStatus::mutableInstance()->updateStatus(); 0471 } 0472 } 0473 0474 void PGPCardWidget::changeUrlRequested() 0475 { 0476 QString text = mUrl; 0477 while (true) { 0478 bool ok = false; 0479 text = QInputDialog::getText(this, 0480 i18n("Change the URL where the pubkey can be found"), 0481 i18n("New pubkey URL:"), 0482 QLineEdit::Normal, 0483 text, 0484 &ok, 0485 Qt::WindowFlags(), 0486 Qt::ImhLatinOnly); 0487 if (!ok) { 0488 return; 0489 } 0490 // Some additional restrictions imposed by gnupg 0491 if (text.size() > 254) { 0492 KMessageBox::error(this, i18nc("@info", "The size of the URL may not exceed 254 characters.")); 0493 } 0494 break; 0495 } 0496 0497 const auto pgpCard = ReaderStatus::instance()->getCard<OpenPGPCard>(mRealSerial); 0498 if (!pgpCard) { 0499 KMessageBox::error(this, i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(mRealSerial))); 0500 return; 0501 } 0502 0503 const QByteArray command = QByteArrayLiteral("SCD SETATTR PUBKEY-URL ") + text.toUtf8(); 0504 ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, command, this, [this](const GpgME::Error &err) { 0505 changeUrlResult(err); 0506 }); 0507 } 0508 0509 void PGPCardWidget::changeUrlResult(const GpgME::Error &err) 0510 { 0511 if (err) { 0512 KMessageBox::error(this, i18nc("@info", "URL change failed: %1", Formatting::errorAsString(err))); 0513 return; 0514 } 0515 if (!err.isCanceled()) { 0516 KMessageBox::information(this, i18nc("@info", "URL successfully changed."), i18nc("@title", "Success")); 0517 ReaderStatus::mutableInstance()->updateStatus(); 0518 } 0519 } 0520 0521 void PGPCardWidget::createKeyFromCardKeys() 0522 { 0523 auto cmd = new CreateOpenPGPKeyFromCardKeysCommand(mRealSerial, OpenPGPCard::AppName, this); 0524 this->setEnabled(false); 0525 connect(cmd, &CreateOpenPGPKeyFromCardKeysCommand::finished, this, [this]() { 0526 this->setEnabled(true); 0527 }); 0528 cmd->start(); 0529 } 0530 0531 void PGPCardWidget::createCSR(const std::string &keyref) 0532 { 0533 auto cmd = new CreateCSRForCardKeyCommand(keyref, mRealSerial, OpenPGPCard::AppName, this); 0534 this->setEnabled(false); 0535 connect(cmd, &CreateCSRForCardKeyCommand::finished, this, [this]() { 0536 this->setEnabled(true); 0537 }); 0538 cmd->start(); 0539 } 0540 0541 void PGPCardWidget::generateKey(const std::string &keyref) 0542 { 0543 auto cmd = new OpenPGPGenerateCardKeyCommand(keyref, mRealSerial, this); 0544 this->setEnabled(false); 0545 connect(cmd, &OpenPGPGenerateCardKeyCommand::finished, this, [this]() { 0546 this->setEnabled(true); 0547 }); 0548 cmd->start(); 0549 } 0550 0551 #include "pgpcardwidget.moc" 0552 0553 #include "moc_pgpcardwidget.cpp"