diff --git a/src/commands/cardcommand.cpp b/src/commands/cardcommand.cpp index 3f4219d8b..4acd2d973 100644 --- a/src/commands/cardcommand.cpp +++ b/src/commands/cardcommand.cpp @@ -1,55 +1,74 @@ /* commands/cardcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "cardcommand.h" #include "cardcommand_p.h" +#include + #include using namespace Kleo; CardCommand::Private *CardCommand::d_func() { return static_cast(d.get()); } const CardCommand::Private *CardCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() CardCommand::Private::Private(CardCommand *qq, const std::string &serialNumber, QWidget *parent) : Command::Private(qq, parent) , serialNumber_(serialNumber) { } CardCommand::Private::~Private() { } +void CardCommand::Private::doFinish() +{ + if (autoResetCardToOpenPGP) { + SmartCard::ReaderStatus::switchCardBackToOpenPGPApp(serialNumber()); + } +} + CardCommand::CardCommand(const std::string &serialNumber, QWidget *parent) : Command(new Private(this, serialNumber, parent)) { } CardCommand::CardCommand(Private *pp) : Command(pp) { } CardCommand::~CardCommand() { } +void CardCommand::setAutoResetCardToOpenPGP(bool autoReset) +{ + d->autoResetCardToOpenPGP = autoReset; +} + +bool CardCommand::autoResetCardToOpenPGP() const +{ + return d->autoResetCardToOpenPGP; +} + #undef q_func #undef d_func diff --git a/src/commands/cardcommand.h b/src/commands/cardcommand.h index ddc81c986..15dbbd0fa 100644 --- a/src/commands/cardcommand.h +++ b/src/commands/cardcommand.h @@ -1,35 +1,38 @@ /* commands/cardcommand.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "command.h" namespace Kleo { class CardCommand : public Command { Q_OBJECT public: explicit CardCommand(const std::string &serialNumber, QWidget *parent); ~CardCommand() override; + void setAutoResetCardToOpenPGP(bool autoReset); + bool autoResetCardToOpenPGP() const; + protected: class Private; inline Private *d_func(); inline const Private *d_func() const; protected: explicit CardCommand(Private *pp); }; } // namespace Kleo diff --git a/src/commands/cardcommand_p.h b/src/commands/cardcommand_p.h index 58b9b11af..3071c617f 100644 --- a/src/commands/cardcommand_p.h +++ b/src/commands/cardcommand_p.h @@ -1,41 +1,45 @@ /* commands/cardcommand_p.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "cardcommand.h" #include "command_p.h" class Kleo::CardCommand::Private : public Command::Private { friend class ::Kleo::CardCommand; Kleo::CardCommand *q_func() const { return static_cast(q); } public: explicit Private(CardCommand *qq, const std::string &serialNumber, QWidget *parent); ~Private() override; std::string serialNumber() const { return serialNumber_; } protected: void setSerialNumber(const std::string &serialNumber) { serialNumber_ = serialNumber; } +private: + void doFinish() override; + private: std::string serialNumber_; + bool autoResetCardToOpenPGP = true; }; diff --git a/src/commands/certificatetopivcardcommand.cpp b/src/commands/certificatetopivcardcommand.cpp index da914f610..94abfd990 100644 --- a/src/commands/certificatetopivcardcommand.cpp +++ b/src/commands/certificatetopivcardcommand.cpp @@ -1,265 +1,266 @@ /* commands/certificatetopivcardcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certificatetopivcardcommand.h" #include "cardcommand_p.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "utils/writecertassuantransaction.h" #include #include #include #include #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class CertificateToPIVCardCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::CertificateToPIVCardCommand; CertificateToPIVCardCommand *q_func() const { return static_cast(q); } public: explicit Private(CertificateToPIVCardCommand *qq, const std::string &slot, const std::string &serialno); ~Private() override; private: void start(); void startCertificateToPIVCard(); void authenticate(); void authenticationFinished(); void authenticationCanceled(); private: std::string cardSlot; Key certificate; bool hasBeenCanceled = false; }; CertificateToPIVCardCommand::Private *CertificateToPIVCardCommand::d_func() { return static_cast(d.get()); } const CertificateToPIVCardCommand::Private *CertificateToPIVCardCommand::d_func() const { return static_cast(d.get()); } #define q q_func() #define d d_func() CertificateToPIVCardCommand::Private::Private(CertificateToPIVCardCommand *qq, const std::string &slot, const std::string &serialno) : CardCommand::Private(qq, serialno, nullptr) , cardSlot(slot) { } CertificateToPIVCardCommand::Private::~Private() { } namespace { static Key getCertificateToWriteToPIVCard(const std::string &cardSlot, const std::shared_ptr &card) { if (!cardSlot.empty()) { const std::string cardKeygrip = card->keyInfo(cardSlot).grip; const auto certificate = KeyCache::instance()->findSubkeyByKeyGrip(cardKeygrip).parent(); if (certificate.isNull() || certificate.protocol() != GpgME::CMS) { return Key(); } if ((cardSlot == PIVCard::pivAuthenticationKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::cardAuthenticationKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::digitalSignatureKeyRef() && certificate.canSign()) || (cardSlot == PIVCard::keyManagementKeyRef() && certificate.canEncrypt())) { return certificate; } } return Key(); } } void CertificateToPIVCardCommand::Private::start() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::Private::start()"; const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } certificate = getCertificateToWriteToPIVCard(cardSlot, pivCard); if (certificate.isNull()) { error(i18n("Sorry! No suitable certificate to write to this card slot was found.")); finished(); return; } const QString certificateInfo = i18nc("X.509 certificate DN (validity, created: date)", "%1 (%2, created: %3)", DN(certificate.userID(0).id()).prettyDN(), Formatting::complianceStringShort(certificate), Formatting::creationDateString(certificate)); const QString message = i18nc( "@info %1 name of card slot, %2 serial number of card", "

Please confirm that you want to write the following certificate to the %1 slot of card %2:

" "
%3
", PIVCard::keyDisplayName(cardSlot), QString::fromStdString(serialNumber()), certificateInfo); auto confirmButton = KStandardGuiItem::ok(); confirmButton.setText(i18nc("@action:button", "Write certificate")); confirmButton.setToolTip(QString()); const auto choice = KMessageBox::questionTwoActions( parentWidgetOrView(), message, i18nc("@title:window", "Write certificate to card"), confirmButton, KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::WindowModal); if (choice != KMessageBox::ButtonCode::PrimaryAction) { finished(); return; } startCertificateToPIVCard(); } void CertificateToPIVCardCommand::Private::startCertificateToPIVCard() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::Private::startCertificateToPIVCard()"; auto ctx = Context::createForProtocol(GpgME::CMS); QGpgME::QByteArrayDataProvider dp; Data data(&dp); const Error err = ctx->exportPublicKeys(certificate.primaryFingerprint(), data); if (err) { error(i18nc("@info", "Exporting the certificate failed: %1", Formatting::errorAsString(err))); finished(); return; } const QByteArray certificateData = dp.data(); const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const QByteArray command = QByteArrayLiteral("SCD WRITECERT ") + QByteArray::fromStdString(cardSlot); auto transaction = std::unique_ptr(new WriteCertAssuanTransaction(certificateData)); ReaderStatus::mutableInstance()->startTransaction( pivCard, command, q_func(), [this](const GpgME::Error &err) { q->certificateToPIVCardDone(err); }, std::move(transaction)); } void CertificateToPIVCardCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); + cmd->setAutoResetCardToOpenPGP(false); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void CertificateToPIVCardCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticationFinished()"; if (!hasBeenCanceled) { startCertificateToPIVCard(); } } void CertificateToPIVCardCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } CertificateToPIVCardCommand::CertificateToPIVCardCommand(const std::string& cardSlot, const std::string &serialno) : CardCommand(new Private(this, cardSlot, serialno)) { } CertificateToPIVCardCommand::~CertificateToPIVCardCommand() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::~CertificateToPIVCardCommand()"; } void CertificateToPIVCardCommand::certificateToPIVCardDone(const Error &err) { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::certificateToPIVCardDone():" << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err) { #ifdef GPG_ERROR_HAS_NO_AUTH // gpgme 1.13 reports "BAD PIN" instead of "NO AUTH" if (err.code() == GPG_ERR_NO_AUTH || err.code() == GPG_ERR_BAD_PIN) { d->authenticate(); return; } #endif d->error(i18nc("@info", "Writing the certificate to the card failed: %1", Formatting::errorAsString(err))); } else if (!err.isCanceled()) { d->success(i18nc("@info", "Writing the certificate to the card succeeded.")); ReaderStatus::mutableInstance()->updateStatus(); } d->finished(); } void CertificateToPIVCardCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "CertificateToPIVCardCommand::doStart()"; d->start(); } void CertificateToPIVCardCommand::doCancel() { } #undef q_func #undef d_func diff --git a/src/commands/command_p.h b/src/commands/command_p.h index 27846b343..6d6629e81 100644 --- a/src/commands/command_p.h +++ b/src/commands/command_p.h @@ -1,119 +1,125 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/command_p.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include "command.h" #include "view/keylistcontroller.h" #include #include #include #include #include #include #include #include class Kleo::Command::Private { friend class ::Kleo::Command; protected: Command *const q; public: explicit Private(Command *qq); explicit Private(Command *qq, KeyListController *controller); explicit Private(Command *qq, QWidget *parent); virtual ~Private(); QAbstractItemView *view() const { return view_; } QWidget *parentWidgetOrView() const { if (parentWidget_) { return parentWidget_; } else { return view_; } } WId parentWId() const { return parentWId_; } GpgME::Key key() const { return keys_.empty() ? GpgME::Key{} : keys_.front(); } std::vector keys() const { return keys_; } void finished() { Q_EMIT q->finished(); + doFinish(); if (autoDelete) { q->deleteLater(); } } void canceled() { Q_EMIT q->canceled(); finished(); } void error(const QString &text, const QString &caption = QString(), KMessageBox::Options options = KMessageBox::Notify) const { if (parentWId_) { KMessageBox::errorWId(parentWId_, text, caption, options); } else { KMessageBox::error(parentWidgetOrView(), text, caption, options); } } void success(const QString &text, const QString &caption = {}, KMessageBox::Options options = KMessageBox::Notify) const { static const QString noDontShowAgainName{}; const QString title = caption.isEmpty() ? i18nc("@title:window", "Success") : caption; if (parentWId_) { KMessageBox::informationWId(parentWId_, text, title, noDontShowAgainName, options); } else { KMessageBox::information(parentWidgetOrView(), text, title, noDontShowAgainName, options); } } void information(const QString &text, const QString &caption = QString(), const QString &dontShowAgainName = QString(), KMessageBox::Options options = KMessageBox::Notify) const { if (parentWId_) { KMessageBox::informationWId(parentWId_, text, caption, dontShowAgainName, options); } else { KMessageBox::information(parentWidgetOrView(), text, caption, dontShowAgainName, options); } } void applyWindowID(QWidget *w) const { q->applyWindowID(w); } +private: + virtual void doFinish() + { + } + private: bool autoDelete : 1; bool warnWhenRunningAtShutdown : 1; std::vector keys_; QPointer view_; QPointer parentWidget_; WId parentWId_ = 0; QPointer controller_; }; diff --git a/src/commands/keytocardcommand.cpp b/src/commands/keytocardcommand.cpp index 7c323b9be..f50179472 100644 --- a/src/commands/keytocardcommand.cpp +++ b/src/commands/keytocardcommand.cpp @@ -1,736 +1,737 @@ /* commands/keytocardcommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2020,2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keytocardcommand.h" #include "cardcommand_p.h" #include "authenticatepivcardapplicationcommand.h" #include "smartcard/algorithminfo.h" #include "smartcard/openpgpcard.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "smartcard/utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; namespace { QString cardDisplayName(const std::shared_ptr &card) { return i18nc("smartcard application - serial number of smartcard", "%1 - %2", displayAppName(card->appName()), card->displaySerialNumber()); } } class KeyToCardCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::KeyToCardCommand; KeyToCardCommand *q_func() const { return static_cast(q); } public: explicit Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey); explicit Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName); private: void start(); void startKeyToOpenPGPCard(); Subkey getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &card); void startKeyToPIVCard(); void authenticate(); void authenticationFinished(); void authenticationCanceled(); void keyToCardDone(const GpgME::Error &err); void keyToPIVCardDone(const GpgME::Error &err); void updateDone(); void keyHasBeenCopiedToCard(); bool backupKey(); std::vector readSecretKeyFile(); bool writeSecretKeyBackup(const QString &filename, const std::vector &keydata); void startDeleteSecretKeyLocally(); void deleteSecretKeyLocallyFinished(const GpgME::Error &err); private: std::string appName; GpgME::Subkey subkey; std::string cardSlot; bool overwriteExistingAlreadyApproved = false; bool hasBeenCanceled = false; QMetaObject::Connection updateConnection; }; KeyToCardCommand::Private *KeyToCardCommand::d_func() { return static_cast(d.get()); } const KeyToCardCommand::Private *KeyToCardCommand::d_func() const { return static_cast(d.get()); } #define q q_func() #define d d_func() KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const GpgME::Subkey &subkey_) : CardCommand::Private(qq, "", nullptr) , subkey(subkey_) { } KeyToCardCommand::Private::Private(KeyToCardCommand *qq, const std::string &slot, const std::string &serialNumber, const std::string &appName_) : CardCommand::Private(qq, serialNumber, nullptr) , appName(appName_) , cardSlot(slot) { } namespace { static std::shared_ptr getCardToTransferSubkeyTo(const Subkey &subkey, QWidget *parent) { const std::vector > suitableCards = KeyToCardCommand::getSuitableCards(subkey); if (suitableCards.empty()) { return std::shared_ptr(); } else if (suitableCards.size() == 1) { return suitableCards[0]; } QStringList options; for (const auto &card: suitableCards) { options.push_back(cardDisplayName(card)); } bool ok; const QString choice = QInputDialog::getItem(parent, i18n("Select Card"), i18n("Please select the card the key should be written to:"), options, /* current= */ 0, /* editable= */ false, &ok); if (!ok) { return std::shared_ptr(); } const int index = options.indexOf(choice); return suitableCards[index]; } } void KeyToCardCommand::Private::start() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::start()"; if (!subkey.isNull() && serialNumber().empty()) { const auto card = getCardToTransferSubkeyTo(subkey, parentWidgetOrView()); if (!card) { finished(); return; } setSerialNumber(card->serialNumber()); appName = card->appName(); } const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (card->appName() == SmartCard::OpenPGPCard::AppName) { startKeyToOpenPGPCard(); } else if (card->appName() == SmartCard::PIVCard::AppName) { startKeyToPIVCard(); } else { error(xi18nc("@info", "Sorry! Writing keys to the card %1 is not supported.", cardDisplayName(card))); finished(); return; } } namespace { static std::string getOpenPGPCardSlotForKey(const GpgME::Subkey &subKey, QWidget *parent) { // Check if we need to ask the user for the slot if ((subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt() && !subKey.canAuthenticate()) { // Signing only return OpenPGPCard::pgpSigKeyRef(); } if (subKey.canEncrypt() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canAuthenticate()) { // Encrypt only return OpenPGPCard::pgpEncKeyRef(); } if (subKey.canAuthenticate() && !(subKey.canSign() || subKey.canCertify()) && !subKey.canEncrypt()) { // Auth only return OpenPGPCard::pgpAuthKeyRef(); } // Multiple uses, ask user. QStringList options; std::vector cardSlots; if (subKey.canSign() || subKey.canCertify()) { options.push_back(i18nc("@item:inlistbox", "Signature")); cardSlots.push_back(OpenPGPCard::pgpSigKeyRef()); } if (subKey.canEncrypt()) { options.push_back(i18nc("@item:inlistbox", "Encryption")); cardSlots.push_back(OpenPGPCard::pgpEncKeyRef()); } if (subKey.canAuthenticate()) { options.push_back(i18nc("@item:inlistbox", "Authentication")); cardSlots.push_back(OpenPGPCard::pgpAuthKeyRef()); } bool ok; const QString choice = QInputDialog::getItem(parent, i18n("Select Card Slot"), i18n("Please select the card slot the key should be written to:"), options, /* current= */ 0, /* editable= */ false, &ok); const int choiceIndex = options.indexOf(choice); if (ok && choiceIndex >= 0) { return cardSlots[choiceIndex]; } else { return {}; } } } void KeyToCardCommand::Private::startKeyToOpenPGPCard() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToOpenPGPCard()"; const auto pgpCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pgpCard) { error(i18n("Failed to find the OpenPGP card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (subkey.isNull()) { finished(); return; } if (subkey.parent().protocol() != GpgME::OpenPGP) { error(i18n("Sorry! This key cannot be transferred to an OpenPGP card.")); finished(); return; } cardSlot = getOpenPGPCardSlotForKey(subkey, parentWidgetOrView()); if (cardSlot.empty()) { finished(); return; } // Check if we need to do the overwrite warning. const std::string existingKey = pgpCard->keyFingerprint(cardSlot); if (!existingKey.empty()) { const auto encKeyWarning = (cardSlot == OpenPGPCard::pgpEncKeyRef()) ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString{}; const QString message = i18nc( "@info", "

The card %1 already contains a key in this slot. Continuing will overwrite that key.

" "

If there is no backup the existing key will be irrecoverably lost.

", cardDisplayName(pgpCard)) + i18n("The existing key has the fingerprint:") + QStringLiteral("
%1
").arg(Formatting::prettyID(existingKey.c_str())) + encKeyWarning; const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Overwrite existing key"), KGuiItem{i18nc("@action:button", "Overwrite Existing Key")}, KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { finished(); return; } } // Now do the deed const auto time = QDateTime::fromSecsSinceEpoch(quint32(subkey.creationTime()), Qt::UTC); const auto timestamp = time.toString(QStringLiteral("yyyyMMdd'T'HHmmss")); const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 %3 %4") .arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber()), QString::fromStdString(cardSlot), timestamp); ReaderStatus::mutableInstance()->startSimpleTransaction(pgpCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) { keyToCardDone(err); }); } namespace { static std::vector getSigningCertificates() { std::vector signingCertificates = KeyCache::instance()->secretKeys(); const auto it = std::remove_if(signingCertificates.begin(), signingCertificates.end(), [](const Key &key) { return ! (key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canSign() && !key.subkey(0).canEncrypt() && key.subkey(0).isSecret() && !key.subkey(0).isCardKey()); }); signingCertificates.erase(it, signingCertificates.end()); return signingCertificates; } static std::vector getEncryptionCertificates() { std::vector encryptionCertificates = KeyCache::instance()->secretKeys(); const auto it = std::remove_if(encryptionCertificates.begin(), encryptionCertificates.end(), [](const Key &key) { return ! (key.protocol() == GpgME::CMS && !key.subkey(0).isNull() && key.subkey(0).canEncrypt() && key.subkey(0).isSecret() && !key.subkey(0).isCardKey()); }); encryptionCertificates.erase(it, encryptionCertificates.end()); return encryptionCertificates; } } Subkey KeyToCardCommand::Private::getSubkeyToTransferToPIVCard(const std::string &cardSlot, const std::shared_ptr &/*card*/) { if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) { return Subkey(); } const std::vector certificates = cardSlot == PIVCard::cardAuthenticationKeyRef() ? getSigningCertificates() : getEncryptionCertificates(); if (certificates.empty()) { error(i18n("Sorry! No suitable certificate to write to this card slot was found.")); return Subkey(); } auto dialog = new KeySelectionDialog(parentWidgetOrView()); dialog->setWindowTitle(i18nc("@title:window", "Select Certificate")); dialog->setText(i18n("Please select the certificate whose key pair you want to write to the card:")); dialog->setKeys(certificates); if (dialog->exec() == QDialog::Rejected) { return Subkey(); } return dialog->selectedKey().subkey(0); } void KeyToCardCommand::Private::startKeyToPIVCard() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::Private::startKeyToPIVCard()"; const auto pivCard = SmartCard::ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } if (cardSlot != PIVCard::cardAuthenticationKeyRef() && cardSlot != PIVCard::keyManagementKeyRef()) { // key to card is only supported for the Card Authentication key and the Key Management key finished(); return; } if (subkey.isNull()) { subkey = getSubkeyToTransferToPIVCard(cardSlot, pivCard); } if (subkey.isNull()) { finished(); return; } if (subkey.parent().protocol() != GpgME::CMS) { error(i18n("Sorry! This key cannot be transferred to a PIV card.")); finished(); return; } if (!subkey.canEncrypt() && !subkey.canSign()) { error(i18n("Sorry! Only encryption keys and signing keys can be transferred to a PIV card.")); finished(); return; } // Check if we need to do the overwrite warning. if (!overwriteExistingAlreadyApproved) { const std::string existingKey = pivCard->keyInfo(cardSlot).grip; if (!existingKey.empty() && (existingKey != subkey.keyGrip())) { const QString decryptionWarning = (cardSlot == PIVCard::keyManagementKeyRef()) ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString(); const QString message = i18nc( "@info", "

The card %1 already contains a key in this slot. Continuing will overwrite that key.

" "

If there is no backup the existing key will be irrecoverably lost.

", cardDisplayName(pivCard)) + i18n("The existing key has the key grip:") + QStringLiteral("
%1
").arg(QString::fromStdString(existingKey)) + decryptionWarning; const auto choice = KMessageBox::warningContinueCancel(parentWidgetOrView(), message, i18nc("@title:window", "Overwrite existing key"), KGuiItem{i18nc("@action:button", "Overwrite Existing Key")}, KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { finished(); return; } overwriteExistingAlreadyApproved = true; } } const QString cmd = QStringLiteral("KEYTOCARD --force %1 %2 %3") .arg(QString::fromLatin1(subkey.keyGrip()), QString::fromStdString(serialNumber())) .arg(QString::fromStdString(cardSlot)); ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, cmd.toUtf8(), q_func(), [this](const GpgME::Error &err) { keyToPIVCardDone(err); }); } void KeyToCardCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); + cmd->setAutoResetCardToOpenPGP(false); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void KeyToCardCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationFinished()"; if (!hasBeenCanceled) { startKeyToPIVCard(); } } void KeyToCardCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } void KeyToCardCommand::Private::updateDone() { disconnect(updateConnection); const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const std::string keyGripOnCard = card->keyInfo(cardSlot).grip; if (keyGripOnCard != subkey.keyGrip()) { qCWarning(KLEOPATRA_LOG) << q << __func__ << "KEYTOCARD succeeded, but key on card doesn't match copied key"; error(i18nc("@info", "Copying the key to the card failed.")); finished(); return; } keyHasBeenCopiedToCard(); } void KeyToCardCommand::Private::keyHasBeenCopiedToCard() { const auto answer = KMessageBox::questionTwoActionsCancel( parentWidgetOrView(), xi18nc("@info", "The key has been copied to the card." "Do you want to delete the copy of the key stored on this computer?"), i18nc("@title:window", "Success"), KGuiItem{i18nc("@action:button", "Create Backup and Delete Key")}, KGuiItem{i18nc("@action:button", "Delete Key")}, KGuiItem{i18nc("@action:button", "Keep Key")}); if (answer == KMessageBox::ButtonCode::Cancel) { finished(); return; } if (answer == KMessageBox::ButtonCode::PrimaryAction) { if (!backupKey()) { finished(); return; } } startDeleteSecretKeyLocally(); } namespace { QString gnupgPrivateKeyBackupExtension() { return QStringLiteral(".gpgsk"); } QString proposeFilename(const Subkey &subkey) { QString filename; const auto key = subkey.parent(); auto name = Formatting::prettyName(key); if (name.isEmpty()) { name = Formatting::prettyEMail(key); } const auto shortKeyID = Formatting::prettyKeyID(key.shortKeyID()); const auto shortSubkeyID = Formatting::prettyKeyID(QByteArray{subkey.keyID()}.right(8).constData()); const auto usage = Formatting::usageString(subkey).replace(QLatin1String{", "}, QLatin1String{"_"}); /* Not translated so it's better to use in tutorials etc. */ filename = ((shortKeyID == shortSubkeyID) // ? QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3"}.arg(name, shortKeyID, usage) : QStringView{u"%1_%2_SECRET_KEY_BACKUP_%3_%4"}.arg(name, shortKeyID, shortSubkeyID, usage)); filename.replace(u'/', u'_'); return QDir{ApplicationState::lastUsedExportDirectory()}.filePath(filename + gnupgPrivateKeyBackupExtension()); } QString requestPrivateKeyBackupFilename(const QString &proposedFilename, QWidget *parent) { auto filename = FileDialog::getSaveFileNameEx( parent, i18nc("@title:window", "Backup Secret Key"), QStringLiteral("imp"), proposedFilename, i18nc("description of filename filter", "Secret Key Backup Files") + QLatin1String{" (*.gpgsk)"}); if (!filename.isEmpty()) { const QFileInfo fi{filename}; if (fi.suffix().isEmpty()) { filename += gnupgPrivateKeyBackupExtension(); } ApplicationState::setLastUsedExportDirectory(filename); } return filename; } } bool KeyToCardCommand::Private::backupKey() { static const QByteArray backupInfoName = "Backup-info:"; auto keydata = readSecretKeyFile(); if (keydata.empty()) { return false; } const auto filename = requestPrivateKeyBackupFilename(proposeFilename(subkey), parentWidgetOrView()); if (filename.isEmpty()) { return false; } // remove old backup info Kleo::erase_if(keydata, [](const auto &line) { return line.startsWith(backupInfoName); }); // prepend new backup info const QByteArrayList backupInfo = { backupInfoName, subkey.keyGrip(), QDateTime::currentDateTimeUtc().toString(Qt::ISODate).toUtf8(), "Kleopatra", Formatting::prettyNameAndEMail(subkey.parent()).toUtf8(), }; keydata.insert(keydata.begin(), backupInfo.join(' ') + '\n'); return writeSecretKeyBackup(filename, keydata); } std::vector KeyToCardCommand::Private::readSecretKeyFile() { const auto filename = QString::fromLatin1(subkey.keyGrip()) + QLatin1String{".key"}; const auto path = QDir{Kleo::gnupgPrivateKeysDirectory()}.filePath(filename); QFile file{path}; if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { error(xi18n("Cannot open the private key file %1 for reading.", path)); return {}; } std::vector lines; while (!file.atEnd()) { lines.push_back(file.readLine()); } if (lines.empty()) { error(xi18n("The private key file %1 is empty.", path)); } return lines; } bool KeyToCardCommand::Private::writeSecretKeyBackup(const QString &filename, const std::vector &keydata) { QSaveFile file{filename}; // open the file in binary format because we want to write Unix line endings if (!file.open(QIODevice::WriteOnly)) { error(xi18n("Cannot open the file %1 for writing.", filename)); return false; } for (const auto &line : keydata) { file.write(line); } if (!file.commit()) { error(xi18n("Writing the backup of the secret key to %1 failed.", filename)); return false; }; return true; } void KeyToCardCommand::Private::startDeleteSecretKeyLocally() { const auto card = SmartCard::ReaderStatus::instance()->getCard(serialNumber(), appName); if (!card) { error(i18n("Failed to find the card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const auto answer = KMessageBox::questionTwoActions( parentWidgetOrView(), xi18n("Do you really want to delete the local copy of the secret key?"), i18nc("@title:window", "Confirm Deletion"), KStandardGuiItem::del(), KStandardGuiItem::cancel(), {}, KMessageBox::Notify | KMessageBox::Dangerous); if (answer != KMessageBox::ButtonCode::PrimaryAction) { finished(); return; } const auto cmd = QByteArray{"DELETE_KEY --force "} + subkey.keyGrip(); ReaderStatus::mutableInstance()->startSimpleTransaction(card, cmd, q, [this](const GpgME::Error &err) { deleteSecretKeyLocallyFinished(err); }); } void KeyToCardCommand::Private::deleteSecretKeyLocallyFinished(const GpgME::Error &err) { if (err) { error(xi18nc("@info", "Failed to delete the key:%1", Formatting::errorAsString(err))); } ReaderStatus::mutableInstance()->updateStatus(); success(i18nc("@info", "Successfully copied the key to the card.")); finished(); } KeyToCardCommand::KeyToCardCommand(const GpgME::Subkey &subkey) : CardCommand(new Private(this, subkey)) { } KeyToCardCommand::KeyToCardCommand(const std::string& cardSlot, const std::string &serialNumber, const std::string &appName) : CardCommand(new Private(this, cardSlot, serialNumber, appName)) { } KeyToCardCommand::~KeyToCardCommand() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::~KeyToCardCommand()"; } namespace { bool cardSupportsKeyAlgorithm(const std::shared_ptr &card, const std::string &keyAlgo) { if (card->appName() == OpenPGPCard::AppName) { const auto pgpCard = static_cast(card.get()); const auto cardAlgos = pgpCard->supportedAlgorithms(); return Kleo::any_of(cardAlgos, [keyAlgo](const auto &algo) { return (keyAlgo == algo.id) // || (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpEncKeyRef())) || (keyAlgo == OpenPGPCard::getAlgorithmName(algo.id, OpenPGPCard::pgpSigKeyRef())); }); } return false; } } // static std::vector> KeyToCardCommand::getSuitableCards(const GpgME::Subkey &subkey) { std::vector> suitableCards; if (subkey.isNull() || subkey.parent().protocol() != GpgME::OpenPGP) { return suitableCards; } const auto keyAlgo = subkey.algoName(); Kleo::copy_if(ReaderStatus::instance()->getCards(), std::back_inserter(suitableCards), [keyAlgo](const auto &card) { return cardSupportsKeyAlgorithm(card, keyAlgo); }); return suitableCards; } void KeyToCardCommand::Private::keyToCardDone(const GpgME::Error &err) { if (!err && !err.isCanceled()) { updateConnection = connect(ReaderStatus::instance(), &ReaderStatus::updateFinished, q, [this]() { updateDone(); }); ReaderStatus::mutableInstance()->updateCard(serialNumber(), appName); return; } if (err) { error(xi18nc("@info", "Copying the key to the card failed:%1", Formatting::errorAsString(err))); } finished(); } void KeyToCardCommand::Private::keyToPIVCardDone(const GpgME::Error &err) { qCDebug(KLEOPATRA_LOG) << q << __func__ << Formatting::errorAsString(err) << "(" << err.code() << ")"; #ifdef GPG_ERROR_HAS_NO_AUTH // gpgme 1.13 reports "BAD PIN" instead of "NO AUTH" if (err.code() == GPG_ERR_NO_AUTH || err.code() == GPG_ERR_BAD_PIN) { authenticate(); return; } #endif keyToCardDone(err); } void KeyToCardCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "KeyToCardCommand::doStart()"; d->start(); } void KeyToCardCommand::doCancel() { } #undef q_func #undef d_func diff --git a/src/commands/pivgeneratecardkeycommand.cpp b/src/commands/pivgeneratecardkeycommand.cpp index c5367bf5f..603acbe5c 100644 --- a/src/commands/pivgeneratecardkeycommand.cpp +++ b/src/commands/pivgeneratecardkeycommand.cpp @@ -1,258 +1,259 @@ /* commands/pivgeneratecardkeycommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "pivgeneratecardkeycommand.h" #include "cardcommand_p.h" #include "smartcard/algorithminfo.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "dialogs/gencardkeydialog.h" #include #include #include #include #if GPG_ERROR_VERSION_NUMBER >= 0x12400 // 1.36 # define GPG_ERROR_HAS_NO_AUTH #endif #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::SmartCard; using namespace GpgME; class PIVGenerateCardKeyCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::PIVGenerateCardKeyCommand; PIVGenerateCardKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(PIVGenerateCardKeyCommand *qq, const std::string &serialNumber, QWidget *p); ~Private() override; void init(); private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void authenticate(); void authenticationFinished(); void authenticationCanceled(); void generateKey(); void ensureDialogCreated(); private: std::string keyRef; bool overwriteExistingKey = false; std::string algorithm; QPointer dialog; bool hasBeenCanceled = false; }; PIVGenerateCardKeyCommand::Private *PIVGenerateCardKeyCommand::d_func() { return static_cast(d.get()); } const PIVGenerateCardKeyCommand::Private *PIVGenerateCardKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() PIVGenerateCardKeyCommand::Private::Private(PIVGenerateCardKeyCommand *qq, const std::string &serialNumber, QWidget *p) : CardCommand::Private(qq, serialNumber, p) , dialog() { } PIVGenerateCardKeyCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::Private::~Private()"; } PIVGenerateCardKeyCommand::PIVGenerateCardKeyCommand(const std::string &serialNumber, QWidget *p) : CardCommand(new Private(this, serialNumber, p)) { d->init(); } void PIVGenerateCardKeyCommand::Private::init() { } PIVGenerateCardKeyCommand::~PIVGenerateCardKeyCommand() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::~PIVGenerateCardKeyCommand()"; } void PIVGenerateCardKeyCommand::setKeyRef(const std::string &keyRef) { d->keyRef = keyRef; } void PIVGenerateCardKeyCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::doStart()"; // check if key exists auto pivCard = ReaderStatus::instance()->getCard(d->serialNumber()); if (!pivCard) { d->error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(d->serialNumber()))); d->finished(); return; } auto existingKey = pivCard->keyInfo(d->keyRef).grip; if (!existingKey.empty()) { const QString warningText = i18nc("@info", "

This card already contains a key in this slot. Continuing will overwrite that key.

" "

If there is no backup the existing key will be irrecoverably lost.

") + i18n("The existing key has the ID:") + QStringLiteral("
%1
").arg(QString::fromStdString(existingKey)) + (d->keyRef == PIVCard::keyManagementKeyRef() ? i18n("It will no longer be possible to decrypt past communication encrypted for the existing key.") : QString()); const auto choice = KMessageBox::warningContinueCancel(d->parentWidgetOrView(), warningText, i18nc("@title:window", "Overwrite existing key"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QString(), KMessageBox::Notify | KMessageBox::Dangerous); if (choice != KMessageBox::Continue) { d->finished(); return; } d->overwriteExistingKey = true; } d->ensureDialogCreated(); Q_ASSERT(d->dialog); d->dialog->show(); } void PIVGenerateCardKeyCommand::doCancel() { } void PIVGenerateCardKeyCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); + cmd->setAutoResetCardToOpenPGP(false); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void PIVGenerateCardKeyCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::authenticationFinished()"; if (!hasBeenCanceled) { generateKey(); } } void PIVGenerateCardKeyCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } void PIVGenerateCardKeyCommand::Private::generateKey() { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::generateKey()"; auto pivCard = ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } QByteArrayList command; command << "SCD GENKEY"; if (overwriteExistingKey) { command << "--force"; } if (!algorithm.empty()) { command << "--algo=" + QByteArray::fromStdString(algorithm); } command << "--" << QByteArray::fromStdString(keyRef); ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, command.join(' '), q, [this](const GpgME::Error &err) { slotResult(err); }); } void PIVGenerateCardKeyCommand::Private::slotResult(const GpgME::Error& err) { qCDebug(KLEOPATRA_LOG) << "PIVGenerateCardKeyCommand::slotResult():" << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err) { #ifdef GPG_ERROR_HAS_NO_AUTH if (err.code() == GPG_ERR_NO_AUTH) { authenticate(); return; } #endif error(i18nc("@info", "Generating key failed: %1", Formatting::errorAsString(err))); } else if (!err.isCanceled()) { success(i18nc("@info", "Key successfully generated.")); ReaderStatus::mutableInstance()->updateStatus(); } finished(); } void PIVGenerateCardKeyCommand::Private::slotDialogAccepted() { algorithm = dialog->getKeyParams().algorithm; // assume that we are already authenticated to the card generateKey(); } void PIVGenerateCardKeyCommand::Private::slotDialogRejected() { finished(); } void PIVGenerateCardKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new GenCardKeyDialog(GenCardKeyDialog::KeyAlgorithm, parentWidgetOrView()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setSupportedAlgorithms(PIVCard::supportedAlgorithms(keyRef), "rsa2048"); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } #undef d #undef q #include "moc_pivgeneratecardkeycommand.cpp" diff --git a/src/commands/setpivcardapplicationadministrationkeycommand.cpp b/src/commands/setpivcardapplicationadministrationkeycommand.cpp index 5805f5cbe..de0caed78 100644 --- a/src/commands/setpivcardapplicationadministrationkeycommand.cpp +++ b/src/commands/setpivcardapplicationadministrationkeycommand.cpp @@ -1,222 +1,223 @@ /* commands/setpivcardapplicationadministrationkeycommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "setpivcardapplicationadministrationkeycommand.h" #include "cardcommand_p.h" #include "smartcard/pivcard.h" #include "smartcard/readerstatus.h" #include "commands/authenticatepivcardapplicationcommand.h" #include "dialogs/pivcardapplicationadministrationkeyinputdialog.h" #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace Kleo::SmartCard; using namespace GpgME; class SetPIVCardApplicationAdministrationKeyCommand::Private : public CardCommand::Private { friend class ::Kleo::Commands::SetPIVCardApplicationAdministrationKeyCommand; SetPIVCardApplicationAdministrationKeyCommand *q_func() const { return static_cast(q); } public: explicit Private(SetPIVCardApplicationAdministrationKeyCommand *qq, const std::string &serialNumber, QWidget *p); ~Private() override; void init(); private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void authenticate(); void authenticationFinished(); void authenticationCanceled(); void setAdminKey(); void ensureDialogCreated(); private: QByteArray newAdminKey; QPointer dialog; bool hasBeenCanceled = false; }; SetPIVCardApplicationAdministrationKeyCommand::Private *SetPIVCardApplicationAdministrationKeyCommand::d_func() { return static_cast(d.get()); } const SetPIVCardApplicationAdministrationKeyCommand::Private *SetPIVCardApplicationAdministrationKeyCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() SetPIVCardApplicationAdministrationKeyCommand::Private::Private(SetPIVCardApplicationAdministrationKeyCommand *qq, const std::string &serialNumber, QWidget *p) : CardCommand::Private(qq, serialNumber, p) , dialog() { } SetPIVCardApplicationAdministrationKeyCommand::Private::~Private() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::Private::~Private()"; } SetPIVCardApplicationAdministrationKeyCommand::SetPIVCardApplicationAdministrationKeyCommand(const std::string &serialNumber, QWidget *p) : CardCommand(new Private(this, serialNumber, p)) { d->init(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::init() { } SetPIVCardApplicationAdministrationKeyCommand::~SetPIVCardApplicationAdministrationKeyCommand() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::~SetPIVCardApplicationAdministrationKeyCommand()"; } void SetPIVCardApplicationAdministrationKeyCommand::doStart() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::doStart()"; d->authenticate(); } void SetPIVCardApplicationAdministrationKeyCommand::doCancel() { } void SetPIVCardApplicationAdministrationKeyCommand::Private::authenticate() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::authenticate()"; auto cmd = new AuthenticatePIVCardApplicationCommand(serialNumber(), parentWidgetOrView()); + cmd->setAutoResetCardToOpenPGP(false); cmd->setPrompt(i18n("Please enter the old PIV Card Application Administration Key in hex-encoded form.")); connect(cmd, &AuthenticatePIVCardApplicationCommand::finished, q, [this]() { authenticationFinished(); }); connect(cmd, &AuthenticatePIVCardApplicationCommand::canceled, q, [this]() { authenticationCanceled(); }); cmd->start(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::authenticationFinished() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::authenticationFinished()"; if (!hasBeenCanceled) { setAdminKey(); } } void SetPIVCardApplicationAdministrationKeyCommand::Private::authenticationCanceled() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::authenticationCanceled()"; hasBeenCanceled = true; canceled(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::setAdminKey() { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::setAdminKey()"; ensureDialogCreated(); Q_ASSERT(dialog); dialog->show(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::ensureDialogCreated() { if (dialog) { return; } dialog = new PIVCardApplicationAdministrationKeyInputDialog(parentWidgetOrView()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setLabelText(newAdminKey.isEmpty() ? i18n("Please enter the new PIV Card Application Administration Key in hex-encoded form. " "The key needs to consist of 24 bytes, i.e. 48 hex-characters.") : i18n("Please enter the new PIV Card Application Administration Key again.")); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void SetPIVCardApplicationAdministrationKeyCommand::Private::slotDialogAccepted() { if (newAdminKey.isEmpty()) { newAdminKey = dialog->adminKey(); dialog = nullptr; setAdminKey(); return; } const QByteArray newAdminKey2 = dialog->adminKey(); if (newAdminKey != newAdminKey2) { error(i18nc("@info", "The two keys you have entered do not match. Please retry.")); newAdminKey.clear(); dialog = nullptr; setAdminKey(); return; } auto pivCard = ReaderStatus::instance()->getCard(serialNumber()); if (!pivCard) { error(i18n("Failed to find the PIV card with the serial number: %1", QString::fromStdString(serialNumber()))); finished(); return; } const QByteArray plusPercentEncodedAdminKey = newAdminKey.toPercentEncoding().replace(' ', '+'); const QByteArray command = QByteArray("SCD SETATTR SET-ADM-KEY ") + plusPercentEncodedAdminKey; ReaderStatus::mutableInstance()->startSimpleTransaction(pivCard, command, q, [this](const GpgME::Error &err) { slotResult(err); }); } void SetPIVCardApplicationAdministrationKeyCommand::Private::slotDialogRejected() { finished(); } void SetPIVCardApplicationAdministrationKeyCommand::Private::slotResult(const GpgME::Error& err) { qCDebug(KLEOPATRA_LOG) << "SetPIVCardApplicationAdministrationKeyCommand::slotResult():" << Formatting::errorAsString(err) << "(" << err.code() << ")"; if (err) { error(i18nc("@info", "Setting the PIV Card Application Administration Key failed: %1", Formatting::errorAsString(err))); } else if (!err.isCanceled()) { success(i18nc("@info", "PIV Card Application Administration Key set successfully.")); ReaderStatus::mutableInstance()->updateStatus(); } finished(); } #undef d #undef q #include "moc_setpivcardapplicationadministrationkeycommand.cpp" diff --git a/src/smartcard/readerstatus.cpp b/src/smartcard/readerstatus.cpp index 0df632839..8c664aaa5 100644 --- a/src/smartcard/readerstatus.cpp +++ b/src/smartcard/readerstatus.cpp @@ -1,1221 +1,1236 @@ /* -*- mode: c++; c-basic-offset:4 -*- smartcard/readerstatus.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2020 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "readerstatus.h" #include "deviceinfowatcher.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "openpgpcard.h" #include "netkeycard.h" #include "pivcard.h" #include "p15card.h" #include #include #include #include #include #include "utils/kdtoolsglobal.h" #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::SmartCard; using namespace GpgME; static ReaderStatus *self = nullptr; #define xtoi_1(p) (*(p) <= '9'? (*(p)- '0'): \ *(p) <= 'F'? (*(p)-'A'+10):(*(p)-'a'+10)) #define xtoi_2(p) ((xtoi_1(p) * 16) + xtoi_1((p)+1)) static const char *flags[] = { "NOCARD", "PRESENT", "ACTIVE", "USABLE", }; static_assert(sizeof flags / sizeof * flags == Card::_NumScdStates, ""); static const char *prettyFlags[] = { "NoCard", "CardPresent", "CardActive", "CardUsable", "CardError", }; static_assert(sizeof prettyFlags / sizeof * prettyFlags == Card::NumStates, ""); Q_DECLARE_METATYPE(GpgME::Error) #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) static QDebug operator<<(QDebug s, const std::string &string) { return s << QString::fromStdString(string); } #endif namespace { static bool gpgHasMultiCardMultiAppSupport() { return !(engineInfo(GpgME::GpgEngine).engineVersion() < "2.3.0"); } static QDebug operator<<(QDebug s, const std::vector< std::pair > &v) { using pair = std::pair; s << '('; for (const pair &p : v) { s << "status(" << QString::fromStdString(p.first) << ") =" << QString::fromStdString(p.second) << '\n'; } return s << ')'; } struct CardApp { std::string serialNumber; std::string appName; }; static void logUnexpectedStatusLine(const std::pair &line, const std::string &prefix = std::string(), const std::string &command = std::string()) { qCWarning(KLEOPATRA_LOG) << (!prefix.empty() ? QString::fromStdString(prefix + ": ") : QString()) << "Unexpected status line" << (!command.empty() ? QString::fromStdString(" on " + command + ":") : QLatin1String(":")) << QString::fromStdString(line.first) << QString::fromStdString(line.second); } static int parse_app_version(const std::string &s) { return std::atoi(s.c_str()); } static Card::PinState parse_pin_state(const QString &s) { bool ok; int i = s.toInt(&ok); if (!ok) { qCDebug(KLEOPATRA_LOG) << "Failed to parse pin state" << s; return Card::UnknownPinState; } switch (i) { case -4: return Card::NullPin; case -3: return Card::PinBlocked; case -2: return Card::NoPin; case -1: return Card::UnknownPinState; default: if (i < 0) { return Card::UnknownPinState; } else { return Card::PinOk; } } } static const std::string scd_getattr_status(std::shared_ptr &gpgAgent, const char *what, Error &err) { std::string cmd = "SCD GETATTR "; cmd += what; return Assuan::sendStatusCommand(gpgAgent, cmd.c_str(), err); } static const std::string getAttribute(std::shared_ptr &gpgAgent, const char *attribute, const char *versionHint) { Error err; const auto result = scd_getattr_status(gpgAgent, attribute, err); if (err) { if (err.code() == GPG_ERR_INV_NAME) { qCDebug(KLEOPATRA_LOG) << "Querying for attribute" << attribute << "not yet supported; needs GnuPG" << versionHint; } else { qCWarning(KLEOPATRA_LOG) << "Running SCD GETATTR " << attribute << " failed:" << err; } return std::string(); } return result; } enum GetCardsAndAppsOptions { WithReportedAppOrder, WithStableAppOrder, }; static std::vector getCardsAndApps(std::shared_ptr &gpgAgent, GetCardsAndAppsOptions options, Error &err) { std::vector result; if (gpgHasMultiCardMultiAppSupport()) { const std::string command = "SCD GETINFO all_active_apps"; const auto statusLines = Assuan::sendStatusLinesCommand(gpgAgent, command.c_str(), err); if (err) { return result; } for (const auto &statusLine: statusLines) { if (statusLine.first == "SERIALNO") { const auto serialNumberAndApps = QByteArray::fromStdString(statusLine.second).split(' '); if (serialNumberAndApps.size() >= 2) { const auto serialNumber = serialNumberAndApps[0]; auto apps = serialNumberAndApps.mid(1); if (options == WithStableAppOrder) { // sort the apps to get a stable order independently of the currently selected application std::sort(apps.begin(), apps.end()); } for (const auto &app: apps) { qCDebug(KLEOPATRA_LOG) << "getCardsAndApps(): Found card" << serialNumber << "with app" << app; result.push_back({ serialNumber.toStdString(), app.toStdString() }); } } else { logUnexpectedStatusLine(statusLine, "getCardsAndApps()", command); } } else { logUnexpectedStatusLine(statusLine, "getCardsAndApps()", command); } } } else { // use SCD SERIALNO to get the currently active card const auto serialNumber = Assuan::sendStatusCommand(gpgAgent, "SCD SERIALNO", err); if (err) { return result; } // use SCD GETATTR APPTYPE to find out which app is active auto appName = scd_getattr_status(gpgAgent, "APPTYPE", err); std::transform(appName.begin(), appName.end(), appName.begin(), [](unsigned char c){ return std::tolower(c); }); if (err) { return result; } result.push_back({ serialNumber, appName }); } return result; } static std::string switchCard(std::shared_ptr &gpgAgent, const std::string &serialNumber, Error &err) { const std::string command = "SCD SWITCHCARD " + serialNumber; const auto statusLines = Assuan::sendStatusLinesCommand(gpgAgent, command.c_str(), err); if (err) { return std::string(); } if (statusLines.size() == 1 && statusLines[0].first == "SERIALNO" && statusLines[0].second == serialNumber) { return serialNumber; } qCWarning(KLEOPATRA_LOG) << "switchCard():" << command << "returned" << statusLines << "(expected:" << "SERIALNO " + serialNumber << ")"; return std::string(); } static std::string switchApp(std::shared_ptr &gpgAgent, const std::string &serialNumber, const std::string &appName, Error &err) { const std::string command = "SCD SWITCHAPP " + appName; const auto statusLines = Assuan::sendStatusLinesCommand(gpgAgent, command.c_str(), err); if (err) { return std::string(); } if (statusLines.size() == 1 && statusLines[0].first == "SERIALNO" && statusLines[0].second.find(serialNumber + ' ' + appName) == 0) { return appName; } qCWarning(KLEOPATRA_LOG) << "switchApp():" << command << "returned" << statusLines << "(expected:" << "SERIALNO " + serialNumber + ' ' + appName + "..." << ")"; return std::string(); } static std::vector getCardApps(std::shared_ptr &gpgAgent, const std::string &serialNumber, Error &err) { const auto cardApps = getCardsAndApps(gpgAgent, WithReportedAppOrder, err); if (err) { return {}; } std::vector apps; kdtools::transform_if(cardApps.begin(), cardApps.end(), std::back_inserter(apps), [](const auto &cardApp) { return cardApp.appName; }, [serialNumber](const auto &cardApp) { return cardApp.serialNumber == serialNumber; }); qCDebug(KLEOPATRA_LOG) << __func__ << "apps:" << apps; return apps; } static void switchCardBackToOpenPGPApp(std::shared_ptr &gpgAgent, const std::string &serialNumber, Error &err) { if (!gpgHasMultiCardMultiAppSupport()) { return; } const auto apps = getCardApps(gpgAgent, serialNumber, err); if (err || apps.empty() || apps[0] == OpenPGPCard::AppName) { return; } if (Kleo::contains(apps, OpenPGPCard::AppName)) { switchApp(gpgAgent, serialNumber, OpenPGPCard::AppName, err); } } static const char * get_openpgp_card_manufacturer_from_serial_number(const std::string &serialno) { qCDebug(KLEOPATRA_LOG) << "get_openpgp_card_manufacturer_from_serial_number(" << serialno.c_str() << ")"; const bool isProperOpenPGPCardSerialNumber = serialno.size() == 32 && serialno.substr(0, 12) == "D27600012401"; if (isProperOpenPGPCardSerialNumber) { const char *sn = serialno.c_str(); const int manufacturerId = xtoi_2(sn + 16)*256 + xtoi_2(sn + 18); switch (manufacturerId) { case 0x0001: return "PPC Card Systems"; case 0x0002: return "Prism"; case 0x0003: return "OpenFortress"; case 0x0004: return "Wewid"; case 0x0005: return "ZeitControl"; case 0x0006: return "Yubico"; case 0x0007: return "OpenKMS"; case 0x0008: return "LogoEmail"; case 0x002A: return "Magrathea"; case 0x1337: return "Warsaw Hackerspace"; case 0xF517: return "FSIJ"; /* 0x0000 and 0xFFFF are defined as test cards per spec, 0xFF00 to 0xFFFE are assigned for use with randomly created serial numbers. */ case 0x0000: case 0xffff: return "test card"; default: return (manufacturerId & 0xff00) == 0xff00 ? "unmanaged S/N range" : "unknown"; } } else { return "unknown"; } } static std::vector get_openpgp_card_supported_algorithms_announced_by_card(std::shared_ptr &gpgAgent) { static constexpr std::string_view cardSlotPrefix = "OPENPGP.1 "; static const std::map algoMapping = { {"cv25519", "curve25519"}, {"cv448", "curve448"}, {"ed25519", "curve25519"}, {"ed448", "curve448"}, {"x448", "curve448"}, }; Error err; const auto lines = Assuan::sendStatusLinesCommand(gpgAgent, "SCD GETATTR KEY-ATTR-INFO", err); if (err) { return {}; } std::vector algos; kdtools::transform_if(lines.cbegin(), lines.cend(), std::back_inserter(algos), [](const auto &line) { auto algo = line.second.substr(cardSlotPrefix.size()); // map a few algorithms to the standard names used by us const auto mapping = algoMapping.find(algo); if (mapping != algoMapping.end()) { algo = mapping->second; } return algo; }, [](const auto &line) { // only consider KEY-ATTR-INFO status lines for the first card slot; // for now, we assume that all card slots support the same algorithms return line.first == "KEY-ATTR-INFO" && line.second.starts_with(cardSlotPrefix); }); // remove duplicate algorithms std::sort(algos.begin(), algos.end()); algos.erase(std::unique(algos.begin(), algos.end()), algos.end()); qCDebug(KLEOPATRA_LOG) << __func__ << "returns" << algos; return algos; } static std::vector get_openpgp_card_supported_algorithms(Card *card, std::shared_ptr &gpgAgent) { // first ask the smart card for the supported algorithms const std::vector announcedAlgos = get_openpgp_card_supported_algorithms_announced_by_card(gpgAgent); if (!announcedAlgos.empty()) { return announcedAlgos; } // otherwise, fall back to hard-coded lists if ((card->cardType() == "yubikey") && (card->cardVersion() >= 0x050203)) { return { "rsa2048", "rsa3072", "rsa4096", "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", "curve25519", }; } else if ((card->cardType() == "zeitcontrol") && (card->appVersion() >= 0x0304)) { return { "rsa2048", "rsa3072", "rsa4096", "brainpoolP256r1", "brainpoolP384r1", "brainpoolP512r1", }; } return { "rsa2048", "rsa3072", "rsa4096" }; } static bool isOpenPGPCardSerialNumber(const std::string &serialNumber) { return serialNumber.size() == 32 && serialNumber.substr(0, 12) == "D27600012401"; } static const std::string getDisplaySerialNumber(std::shared_ptr &gpgAgent, Error &err) { const auto displaySerialNumber = scd_getattr_status(gpgAgent, "$DISPSERIALNO", err); if (err && err.code() != GPG_ERR_INV_NAME) { qCWarning(KLEOPATRA_LOG) << "Running SCD GETATTR $DISPSERIALNO failed:" << err; } return displaySerialNumber; } static void setDisplaySerialNumber(Card *card, std::shared_ptr &gpgAgent) { static const QRegularExpression leadingZeros(QStringLiteral("^0*")); Error err; const QString displaySerialNumber = QString::fromStdString(getDisplaySerialNumber(gpgAgent, err)); if (err) { card->setDisplaySerialNumber(QString::fromStdString(card->serialNumber())); return; } if (isOpenPGPCardSerialNumber(card->serialNumber()) && displaySerialNumber.size() == 12) { // add a space between manufacturer id and card id for OpenPGP cards card->setDisplaySerialNumber(displaySerialNumber.left(4) + QLatin1Char(' ') + displaySerialNumber.right(8)); } else { card->setDisplaySerialNumber(displaySerialNumber); } return; } static void learnCardKeyStubs(const Card *card, std::shared_ptr &gpg_agent) { for (const KeyPairInfo &keyInfo : card->keyInfos()) { if (!keyInfo.grip.empty()) { Error err; const auto command = std::string("READKEY --card --no-data -- ") + keyInfo.keyRef; (void)Assuan::sendStatusLinesCommand(gpg_agent, command.c_str(), err); if (err) { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; } } } } static void handle_openpgp_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto pgpCard = new OpenPGPCard(*ci); const auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err.code()) { ci->setStatus(Card::CardError); return; } pgpCard->setCardInfo(info); if (pgpCard->manufacturer().empty()) { // fallback in case MANUFACTURER is not yet included in the card info pgpCard->setManufacturer(get_openpgp_card_manufacturer_from_serial_number(ci->serialNumber())); } setDisplaySerialNumber(pgpCard, gpg_agent); learnCardKeyStubs(pgpCard, gpg_agent); pgpCard->setSupportedAlgorithms(get_openpgp_card_supported_algorithms(pgpCard, gpg_agent)); ci.reset(pgpCard); } static void readKeyPairInfoFromPIVCard(const std::string &keyRef, PIVCard *pivCard, const std::shared_ptr &gpg_agent) { Error err; const std::string command = std::string("SCD READKEY --info-only -- ") + keyRef; const auto keyPairInfoLines = Assuan::sendStatusLinesCommand(gpg_agent, command.c_str(), err); if (err) { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; return; } // this adds the key algorithm (and the key creation date, but that seems to be unset for PIV) to the existing key pair information pivCard->setCardInfo(keyPairInfoLines); } static void readCertificateFromPIVCard(const std::string &keyRef, PIVCard *pivCard, const std::shared_ptr &gpg_agent) { Error err; const std::string command = std::string("SCD READCERT ") + keyRef; const std::string certificateData = Assuan::sendDataCommand(gpg_agent, command.c_str(), err); if (err && err.code() != GPG_ERR_NOT_FOUND) { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; return; } if (certificateData.empty()) { qCDebug(KLEOPATRA_LOG) << "readCertificateFromPIVCard(" << QString::fromStdString(keyRef) << "): No certificate stored on card"; return; } qCDebug(KLEOPATRA_LOG) << "readCertificateFromPIVCard(" << QString::fromStdString(keyRef) << "): Found certificate stored on card"; pivCard->setCertificateData(keyRef, certificateData); } static void handle_piv_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto pivCard = new PIVCard(*ci); const auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err) { ci->setStatus(Card::CardError); return; } pivCard->setCardInfo(info); setDisplaySerialNumber(pivCard, gpg_agent); for (const KeyPairInfo &keyInfo : pivCard->keyInfos()) { if (!keyInfo.grip.empty()) { readKeyPairInfoFromPIVCard(keyInfo.keyRef, pivCard, gpg_agent); readCertificateFromPIVCard(keyInfo.keyRef, pivCard, gpg_agent); } } learnCardKeyStubs(pivCard, gpg_agent); ci.reset(pivCard); } static void handle_p15_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto p15Card = new P15Card(*ci); auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err) { ci->setStatus(Card::CardError); return; } const auto fprs = Assuan::sendStatusLinesCommand(gpg_agent, "SCD GETATTR KEY-FPR", err); if (!err) { info.insert(info.end(), fprs.begin(), fprs.end()); } p15Card->setCardInfo(info); learnCardKeyStubs(p15Card, gpg_agent); setDisplaySerialNumber(p15Card, gpg_agent); ci.reset(p15Card); } static void handle_netkey_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto nkCard = new NetKeyCard(*ci); ci.reset(nkCard); ci->setAppVersion(parse_app_version(scd_getattr_status(gpg_agent, "NKS-VERSION", err))); if (err.code()) { qCWarning(KLEOPATRA_LOG) << "Running SCD GETATTR NKS-VERSION failed:" << err; ci->setErrorMsg(QStringLiteral ("NKS-VERSION failed: ") + Formatting::errorAsString(err)); return; } if (ci->appVersion() < 3) { qCDebug(KLEOPATRA_LOG) << "not a NetKey v3 (or later) card, giving up. Version:" << ci->appVersion(); ci->setErrorMsg(QStringLiteral("NetKey v%1 cards are not supported.").arg(ci->appVersion())); return; } setDisplaySerialNumber(nkCard, gpg_agent); // the following only works for NKS v3... const auto chvStatus = QString::fromStdString( scd_getattr_status(gpg_agent, "CHV-STATUS", err)).split(QLatin1Char(' ')); if (err.code()) { qCDebug(KLEOPATRA_LOG) << "Running SCD GETATTR CHV-STATUS failed:" << err; ci->setErrorMsg(QStringLiteral ("CHV-Status failed: ") + Formatting::errorAsString(err)); return; } std::vector states; states.reserve(chvStatus.count()); // CHV Status for NKS v3 is // Pin1 (Normal pin) Pin2 (Normal PUK) // SigG1 SigG PUK. int num = 0; for (const auto &state: chvStatus) { const auto parsed = parse_pin_state (state); states.push_back(parsed); if (parsed == Card::NullPin) { if (num == 0) { ci->setHasNullPin(true); } } ++num; } nkCard->setPinStates(states); const auto info = Assuan::sendStatusLinesCommand(gpg_agent, "SCD LEARN --force", err); if (err) { ci->setStatus(Card::CardError); return; } nkCard->setCardInfo(info); learnCardKeyStubs(nkCard, gpg_agent); } static std::shared_ptr get_card_status(const std::string &serialNumber, const std::string &appName, std::shared_ptr &gpg_agent) { qCDebug(KLEOPATRA_LOG) << "get_card_status(" << serialNumber << ',' << appName << ',' << gpg_agent.get() << ')'; auto ci = std::shared_ptr(new Card()); if (gpgHasMultiCardMultiAppSupport()) { // select card Error err; const auto result = switchCard(gpg_agent, serialNumber, err); if (err) { if (err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED) { ci->setStatus(Card::NoCard); } else { ci->setStatus(Card::CardError); } return ci; } if (result.empty()) { qCWarning(KLEOPATRA_LOG) << "get_card_status: switching card failed"; ci->setStatus(Card::CardError); return ci; } ci->setStatus(Card::CardPresent); } else { ci->setStatus(Card::CardPresent); } if (gpgHasMultiCardMultiAppSupport()) { // select app Error err; const auto result = switchApp(gpg_agent, serialNumber, appName, err); if (err) { if (err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED) { ci->setStatus(Card::NoCard); } else { ci->setStatus(Card::CardError); } return ci; } if (result.empty()) { qCWarning(KLEOPATRA_LOG) << "get_card_status: switching app failed"; ci->setStatus(Card::CardError); return ci; } } ci->setSerialNumber(serialNumber); ci->setSigningKeyRef(getAttribute(gpg_agent, "$SIGNKEYID", "2.2.18")); ci->setEncryptionKeyRef(getAttribute(gpg_agent, "$ENCRKEYID", "2.2.18")); // Handle different card types if (appName == NetKeyCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found Netkey card" << ci->serialNumber().c_str() << "end"; handle_netkey_card(ci, gpg_agent); } else if (appName == OpenPGPCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found OpenPGP card" << ci->serialNumber().c_str() << "end"; ci->setAuthenticationKeyRef(OpenPGPCard::pgpAuthKeyRef()); handle_openpgp_card(ci, gpg_agent); } else if (appName == PIVCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found PIV card" << ci->serialNumber().c_str() << "end"; handle_piv_card(ci, gpg_agent); } else if (appName == P15Card::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found P15 card" << ci->serialNumber().c_str() << "end"; handle_p15_card(ci, gpg_agent); } else { qCDebug(KLEOPATRA_LOG) << "get_card_status: unhandled application:" << appName; } if (gpgHasMultiCardMultiAppSupport() && appName != OpenPGPCard::AppName) { // switch the card app back to OpenPGP; errors are ignored GpgME::Error dummy; switchCardBackToOpenPGPApp(gpg_agent, serialNumber, dummy); } return ci; } static bool isCardNotPresentError(const GpgME::Error &err) { // see fixup_scd_errors() in gpg-card.c return err && ((err.code() == GPG_ERR_CARD_NOT_PRESENT) || ((err.code() == GPG_ERR_ENODEV || err.code() == GPG_ERR_CARD_REMOVED) && (err.sourceID() == GPG_ERR_SOURCE_SCD))); } static std::vector > update_cardinfo(std::shared_ptr &gpgAgent) { qCDebug(KLEOPATRA_LOG) << "update_cardinfo()"; // ensure that a card is present and that all cards are properly set up { Error err; const char *command = (gpgHasMultiCardMultiAppSupport()) ? "SCD SERIALNO --all" : "SCD SERIALNO"; const std::string serialno = Assuan::sendStatusCommand(gpgAgent, command, err); if (err) { if (isCardNotPresentError(err)) { qCDebug(KLEOPATRA_LOG) << "update_cardinfo: No card present"; return std::vector >(); } else { qCWarning(KLEOPATRA_LOG) << "Running" << command << "failed:" << err; auto ci = std::shared_ptr(new Card()); ci->setStatus(Card::CardError); return std::vector >(1, ci); } } } Error err; const std::vector cardApps = getCardsAndApps(gpgAgent, WithStableAppOrder, err); if (err) { if (isCardNotPresentError(err)) { qCDebug(KLEOPATRA_LOG) << "update_cardinfo: No card present"; return std::vector >(); } else { qCWarning(KLEOPATRA_LOG) << "Getting active apps on all inserted cards failed:" << err; auto ci = std::shared_ptr(new Card()); ci->setStatus(Card::CardError); return std::vector >(1, ci); } } std::vector > cards; for (const auto &cardApp: cardApps) { const auto card = get_card_status(cardApp.serialNumber, cardApp.appName, gpgAgent); cards.push_back(card); } return cards; } } // namespace struct Transaction { CardApp cardApp; QByteArray command; QPointer receiver; ReaderStatus::TransactionFunc slot; AssuanTransaction* assuanTransaction; }; static const Transaction updateTransaction = { { "__all__", "__all__" }, "__update__", nullptr, nullptr, nullptr }; static const Transaction quitTransaction = { { "__all__", "__all__" }, "__quit__", nullptr, nullptr, nullptr }; namespace { class ReaderStatusThread : public QThread { Q_OBJECT public: explicit ReaderStatusThread(QObject *parent = nullptr) : QThread(parent), m_gnupgHomePath(Kleo::gnupgHomeDirectory()), m_transactions(1, updateTransaction) // force initial scan { connect(this, &ReaderStatusThread::oneTransactionFinished, this, &ReaderStatusThread::slotOneTransactionFinished); } std::vector > cardInfos() const { const QMutexLocker locker(&m_mutex); return m_cardInfos; } Card::Status cardStatus(unsigned int slot) const { const QMutexLocker locker(&m_mutex); if (slot < m_cardInfos.size()) { return m_cardInfos[slot]->status(); } else { return Card::NoCard; } } void addTransaction(const Transaction &t) { const QMutexLocker locker(&m_mutex); m_transactions.push_back(t); m_waitForTransactions.wakeOne(); } Q_SIGNALS: void firstCardWithNullPinChanged(const std::string &serialNumber); void anyCardCanLearnKeysChanged(bool); void cardAdded(const std::string &serialNumber, const std::string &appName); void cardChanged(const std::string &serialNumber, const std::string &appName); void cardRemoved(const std::string &serialNumber, const std::string &appName); void updateFinished(); void oneTransactionFinished(const GpgME::Error &err); public Q_SLOTS: void deviceStatusChanged(const QByteArray &details) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[GUI]::deviceStatusChanged(" << details << ")"; addTransaction(updateTransaction); } void ping() { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[GUI]::ping()"; addTransaction(updateTransaction); } void stop() { const QMutexLocker locker(&m_mutex); m_transactions.push_front(quitTransaction); m_waitForTransactions.wakeOne(); } private Q_SLOTS: void slotOneTransactionFinished(const GpgME::Error &err) { std::list ft; KDAB_SYNCHRONIZED(m_mutex) ft.splice(ft.begin(), m_finishedTransactions); for (const Transaction &t : std::as_const(ft)) if (t.receiver && t.slot) { QMetaObject::invokeMethod(t.receiver, [&t, &err]() { t.slot(err); }, Qt::DirectConnection); } } private: void run() override { while (true) { std::shared_ptr gpgAgent; CardApp cardApp; QByteArray command; bool nullSlot = false; AssuanTransaction* assuanTransaction = nullptr; std::list item; std::vector > oldCards; while (!KeyCache::instance()->initialized()) { qCDebug(KLEOPATRA_LOG) << "Waiting for Keycache to be initialized."; sleep(1); } Error err; std::unique_ptr c = Context::createForEngine(AssuanEngine, &err); if (err.code() == GPG_ERR_NOT_SUPPORTED) { return; } gpgAgent = std::shared_ptr(c.release()); KDAB_SYNCHRONIZED(m_mutex) { while (m_transactions.empty()) { // go to sleep waiting for more work: qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[2nd]: waiting for commands"; m_waitForTransactions.wait(&m_mutex); } // splice off the first transaction without // copying, so we own it without really importing // it into this thread (the QPointer isn't // thread-safe): item.splice(item.end(), m_transactions, m_transactions.begin()); // make local copies of the interesting stuff so // we can release the mutex again: cardApp = item.front().cardApp; command = item.front().command; nullSlot = !item.front().slot; // we take ownership of the assuan transaction std::swap(assuanTransaction, item.front().assuanTransaction); oldCards = m_cardInfos; } qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[2nd]: new iteration command=" << command << " ; nullSlot=" << nullSlot; // now, let's see what we got: if (nullSlot && command == quitTransaction.command) { return; // quit } if ((nullSlot && command == updateTransaction.command)) { bool anyError = false; if (cardApp.serialNumber == "__all__" || cardApp.appName == "__all__") { std::vector > newCards = update_cardinfo(gpgAgent); KDAB_SYNCHRONIZED(m_mutex) { m_cardInfos = newCards; } bool anyLC = false; std::string firstCardWithNullPin; for (const auto &newCard: newCards) { const auto serialNumber = newCard->serialNumber(); const auto appName = newCard->appName(); const auto matchingOldCard = std::find_if(oldCards.cbegin(), oldCards.cend(), [serialNumber, appName] (const std::shared_ptr &card) { return card->serialNumber() == serialNumber && card->appName() == appName; }); if (matchingOldCard == oldCards.cend()) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "was added"; Q_EMIT cardAdded(serialNumber, appName); } else { if (*newCard != **matchingOldCard) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "changed"; Q_EMIT cardChanged(serialNumber, appName); } oldCards.erase(matchingOldCard); } if (newCard->canLearnKeys()) { anyLC = true; } if (newCard->hasNullPin() && firstCardWithNullPin.empty()) { firstCardWithNullPin = newCard->serialNumber(); } if (newCard->status() == Card::CardError) { anyError = true; } } for (const auto &oldCard: oldCards) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << oldCard->serialNumber() << "with app" << oldCard->appName() << "was removed"; Q_EMIT cardRemoved(oldCard->serialNumber(), oldCard->appName()); } Q_EMIT firstCardWithNullPinChanged(firstCardWithNullPin); Q_EMIT anyCardCanLearnKeysChanged(anyLC); } else { auto updatedCard = get_card_status(cardApp.serialNumber, cardApp.appName, gpgAgent); const auto serialNumber = updatedCard->serialNumber(); const auto appName = updatedCard->appName(); bool cardWasAdded = false; bool cardWasChanged = false; KDAB_SYNCHRONIZED(m_mutex) { const auto matchingCard = std::find_if(m_cardInfos.begin(), m_cardInfos.end(), [serialNumber, appName](const auto &card) { return card->serialNumber() == serialNumber && card->appName() == appName; }); if (matchingCard == m_cardInfos.end()) { m_cardInfos.push_back(updatedCard); cardWasAdded = true; } else { cardWasChanged = (*updatedCard != **matchingCard); m_cardInfos[std::distance(m_cardInfos.begin(), matchingCard)] = updatedCard; } if (updatedCard->status() == Card::CardError) { anyError = true; } } if (cardWasAdded) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "was added"; Q_EMIT cardAdded(serialNumber, appName); } else if (cardWasChanged) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread: Card" << serialNumber << "with app" << appName << "changed"; Q_EMIT cardChanged(serialNumber, appName); } } if (anyError) { gpgAgent.reset(); } Q_EMIT updateFinished(); } else { GpgME::Error err; if (gpgHasMultiCardMultiAppSupport()) { switchCard(gpgAgent, cardApp.serialNumber, err); if (!err) { switchApp(gpgAgent, cardApp.serialNumber, cardApp.appName, err); } } if (!err) { if (assuanTransaction) { (void)Assuan::sendCommand(gpgAgent, command.constData(), std::unique_ptr(assuanTransaction), err); } else { (void)Assuan::sendCommand(gpgAgent, command.constData(), err); } } KDAB_SYNCHRONIZED(m_mutex) // splice 'item' into m_finishedTransactions: m_finishedTransactions.splice(m_finishedTransactions.end(), item); Q_EMIT oneTransactionFinished(err); } } } private: mutable QMutex m_mutex; QWaitCondition m_waitForTransactions; const QString m_gnupgHomePath; // protected by m_mutex: std::vector > m_cardInfos; std::list m_transactions, m_finishedTransactions; }; } class ReaderStatus::Private : ReaderStatusThread { friend class Kleo::SmartCard::ReaderStatus; ReaderStatus *const q; public: explicit Private(ReaderStatus *qq) : ReaderStatusThread(qq), q(qq), watcher() { KDAB_SET_OBJECT_NAME(watcher); qRegisterMetaType("Kleo::SmartCard::Card::Status"); qRegisterMetaType("GpgME::Error"); connect(this, &::ReaderStatusThread::cardAdded, q, &ReaderStatus::cardAdded); connect(this, &::ReaderStatusThread::cardChanged, q, &ReaderStatus::cardChanged); connect(this, &::ReaderStatusThread::cardRemoved, q, &ReaderStatus::cardRemoved); connect(this, &::ReaderStatusThread::updateFinished, q, &ReaderStatus::updateFinished); connect(this, &::ReaderStatusThread::firstCardWithNullPinChanged, q, &ReaderStatus::firstCardWithNullPinChanged); connect(this, &::ReaderStatusThread::anyCardCanLearnKeysChanged, q, &ReaderStatus::anyCardCanLearnKeysChanged); if (DeviceInfoWatcher::isSupported()) { qCDebug(KLEOPATRA_LOG) << "ReaderStatus::Private: Using new DeviceInfoWatcher"; connect(&devInfoWatcher, &DeviceInfoWatcher::statusChanged, this, &::ReaderStatusThread::deviceStatusChanged); } else { qCDebug(KLEOPATRA_LOG) << "ReaderStatus::Private: Using deprecated FileSystemWatcher"; watcher.whitelistFiles(QStringList(QStringLiteral("reader_*.status"))); watcher.addPath(Kleo::gnupgHomeDirectory()); watcher.setDelay(100); connect(&watcher, &FileSystemWatcher::triggered, this, &::ReaderStatusThread::ping); } } ~Private() override { stop(); if (!wait(100)) { terminate(); wait(); } } private: std::string firstCardWithNullPinImpl() const { const auto cis = cardInfos(); const auto firstWithNullPin = std::find_if(cis.cbegin(), cis.cend(), [](const std::shared_ptr &ci) { return ci->hasNullPin(); }); return firstWithNullPin != cis.cend() ? (*firstWithNullPin)->serialNumber() : std::string(); } bool anyCardCanLearnKeysImpl() const { const auto cis = cardInfos(); return std::any_of(cis.cbegin(), cis.cend(), [](const std::shared_ptr &ci) { return ci->canLearnKeys(); }); } private: FileSystemWatcher watcher; DeviceInfoWatcher devInfoWatcher; }; ReaderStatus::ReaderStatus(QObject *parent) : QObject(parent), d(new Private(this)) { self = this; qRegisterMetaType("std::string"); } ReaderStatus::~ReaderStatus() { self = nullptr; } // slot void ReaderStatus::startMonitoring() { d->start(); if (DeviceInfoWatcher::isSupported()) { connect(&d->devInfoWatcher, &DeviceInfoWatcher::startOfGpgAgentRequested, this, &ReaderStatus::startOfGpgAgentRequested); d->devInfoWatcher.start(); } } // static ReaderStatus *ReaderStatus::mutableInstance() { return self; } // static const ReaderStatus *ReaderStatus::instance() { return self; } Card::Status ReaderStatus::cardStatus(unsigned int slot) const { return d->cardStatus(slot); } std::string ReaderStatus::firstCardWithNullPin() const { return d->firstCardWithNullPinImpl(); } bool ReaderStatus::anyCardCanLearnKeys() const { return d->anyCardCanLearnKeysImpl(); } void ReaderStatus::startSimpleTransaction(const std::shared_ptr &card, const QByteArray &command, QObject *receiver, const TransactionFunc &slot) { const CardApp cardApp = { card->serialNumber(), card->appName() }; const Transaction t = { cardApp, command, receiver, slot, nullptr }; d->addTransaction(t); } void ReaderStatus::startTransaction(const std::shared_ptr &card, const QByteArray &command, QObject *receiver, const TransactionFunc &slot, std::unique_ptr transaction) { const CardApp cardApp = { card->serialNumber(), card->appName() }; const Transaction t = { cardApp, command, receiver, slot, transaction.release() }; d->addTransaction(t); } void ReaderStatus::updateStatus() { d->ping(); } void ReaderStatus::updateCard(const std::string &serialNumber, const std::string &appName) { const CardApp cardApp = { serialNumber, appName }; const Transaction t = { cardApp, updateTransaction.command, nullptr, nullptr, nullptr }; d->addTransaction(t); } std::vector > ReaderStatus::getCards() const { return d->cardInfos(); } std::shared_ptr ReaderStatus::getCard(const std::string &serialNumber, const std::string &appName) const { for (const auto &card: d->cardInfos()) { if (card->serialNumber() == serialNumber && card->appName() == appName) { qCDebug(KLEOPATRA_LOG) << "ReaderStatus::getCard() - Found card with serial number" << serialNumber << "and app" << appName; return card; } } qCWarning(KLEOPATRA_LOG) << "ReaderStatus::getCard() - Did not find card with serial number" << serialNumber << "and app" << appName; return std::shared_ptr(); } // static std::string ReaderStatus::switchCard(std::shared_ptr& ctx, const std::string& serialNumber, Error& err) { return ::switchCard(ctx, serialNumber, err); } // static std::string ReaderStatus::switchApp(std::shared_ptr& ctx, const std::string& serialNumber, const std::string& appName, Error& err) { return ::switchApp(ctx, serialNumber, appName, err); } // static Error ReaderStatus::switchCardAndApp(const std::string &serialNumber, const std::string &appName) { Error err; if (!(engineInfo(GpgEngine).engineVersion() < "2.3.0")) { std::unique_ptr c = Context::createForEngine(AssuanEngine, &err); if (err.code() == GPG_ERR_NOT_SUPPORTED) { return err; } auto assuanContext = std::shared_ptr(c.release()); const auto resultSerialNumber = switchCard(assuanContext, serialNumber, err); if (err || resultSerialNumber != serialNumber) { qCWarning(KLEOPATRA_LOG) << "Switching to card" << QString::fromStdString(serialNumber) << "failed"; if (!err) { err = Error::fromCode(GPG_ERR_UNEXPECTED); } return err; } const auto resultAppName = switchApp(assuanContext, serialNumber, appName, err); if (err || resultAppName != appName) { qCWarning(KLEOPATRA_LOG) << "Switching card to" << QString::fromStdString(appName) << "app failed"; if (!err) { err = Error::fromCode(GPG_ERR_UNEXPECTED); } return err; } } return err; } +// static +Error ReaderStatus::switchCardBackToOpenPGPApp(const std::string &serialNumber) +{ + Error err; + if (gpgHasMultiCardMultiAppSupport()) { + std::unique_ptr c = Context::createForEngine(AssuanEngine, &err); + if (err.code() == GPG_ERR_NOT_SUPPORTED) { + return err; + } + auto assuanContext = std::shared_ptr(c.release()); + ::switchCardBackToOpenPGPApp(assuanContext, serialNumber, err); + } + return err; +} + #include "readerstatus.moc" diff --git a/src/smartcard/readerstatus.h b/src/smartcard/readerstatus.h index e379d49de..f6c0a0277 100644 --- a/src/smartcard/readerstatus.h +++ b/src/smartcard/readerstatus.h @@ -1,92 +1,93 @@ /* -*- mode: c++; c-basic-offset:4 -*- smartcard/readerstatus.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include #include "card.h" #include #include namespace GpgME { class AssuanTransaction; class Context; class Error; } namespace Kleo { namespace SmartCard { class ReaderStatus : public QObject { Q_OBJECT public: explicit ReaderStatus(QObject *parent = nullptr); ~ReaderStatus() override; static const ReaderStatus *instance(); static ReaderStatus *mutableInstance(); using TransactionFunc = std::function; void startSimpleTransaction(const std::shared_ptr &card, const QByteArray &cmd, QObject *receiver, const TransactionFunc &slot); void startTransaction(const std::shared_ptr &card, const QByteArray &cmd, QObject *receiver, const TransactionFunc &slot, std::unique_ptr transaction); Card::Status cardStatus(unsigned int slot) const; std::string firstCardWithNullPin() const; bool anyCardCanLearnKeys() const; std::vector > getCards() const; std::shared_ptr getCard(const std::string &serialNumber, const std::string &appName) const; template std::shared_ptr getCard(const std::string &serialNumber) const { return std::dynamic_pointer_cast(getCard(serialNumber, T::AppName)); } static std::string switchCard(std::shared_ptr &ctx, const std::string &serialNumber, GpgME::Error &err); static std::string switchApp(std::shared_ptr &ctx, const std::string &serialNumber, const std::string &appName, GpgME::Error &err); static GpgME::Error switchCardAndApp(const std::string &serialNumber, const std::string &appName); + static GpgME::Error switchCardBackToOpenPGPApp(const std::string &serialNumber); public Q_SLOTS: void updateStatus(); void updateCard(const std::string &serialNumber, const std::string &appName); void startMonitoring(); Q_SIGNALS: void firstCardWithNullPinChanged(const std::string &serialNumber); void anyCardCanLearnKeysChanged(bool); void cardAdded(const std::string &serialNumber, const std::string &appName); void cardChanged(const std::string &serialNumber, const std::string &appName); void cardRemoved(const std::string &serialNumber, const std::string &appName); void updateFinished(); void startOfGpgAgentRequested(); private: class Private; std::shared_ptr d; }; } // namespace SmartCard } // namespace Kleo Q_DECLARE_METATYPE(Kleo::SmartCard::Card::Status)