diff --git a/src/smartcard/readerstatus.cpp b/src/smartcard/readerstatus.cpp index 499519713..6de5f9a40 100644 --- a/src/smartcard/readerstatus.cpp +++ b/src/smartcard/readerstatus.cpp @@ -1,867 +1,957 @@ /* -*- 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 "keypairinfo.h" #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include "openpgpcard.h" #include "netkeycard.h" #include "pivcard.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils/kdtoolsglobal.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, ""); #if 0 We need this once we have support for multiple readers in scdaemons interface. static unsigned int parseFileName(const QString &fileName, bool *ok) { QRegExp rx(QLatin1String("reader_(\\d+)\\.status")); if (ok) { *ok = false; } if (rx.exactMatch(QFileInfo(fileName).fileName())) { return rx.cap(1).toUInt(ok, 10); } return 0; } #endif Q_DECLARE_METATYPE(GpgME::Error) namespace { static QDebug operator<<(QDebug s, const std::string &string) { return s << QString::fromStdString(string); } static QDebug operator<<(QDebug s, const std::vector< std::pair > &v) { typedef std::pair 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; } } } template static std::unique_ptr gpgagent_transact(std::shared_ptr &gpgAgent, const char *command, std::unique_ptr transaction, Error &err) { qCDebug(KLEOPATRA_LOG) << "gpgagent_transact(" << command << ")"; err = gpgAgent->assuanTransact(command, std::move(transaction)); if (err.code()) { qCDebug(KLEOPATRA_LOG) << "gpgagent_transact(" << command << "):" << QString::fromLocal8Bit(err.asString()); if (err.code() >= GPG_ERR_ASS_GENERAL && err.code() <= GPG_ERR_ASS_UNKNOWN_INQUIRE) { qCDebug(KLEOPATRA_LOG) << "Assuan problem, killing context"; gpgAgent.reset(); } return std::unique_ptr(); } std::unique_ptr t = gpgAgent->takeLastAssuanTransaction(); return std::unique_ptr(dynamic_cast(t.release())); } static std::unique_ptr gpgagent_default_transact(std::shared_ptr &gpgAgent, const char *command, Error &err) { return gpgagent_transact(gpgAgent, command, std::unique_ptr(new DefaultAssuanTransaction), err); } static const std::string gpgagent_data(std::shared_ptr gpgAgent, const char *command, Error &err) { const std::unique_ptr t = gpgagent_default_transact(gpgAgent, command, err); if (t.get()) { qCDebug(KLEOPATRA_LOG) << "gpgagent_data(" << command << "): got" << QString::fromStdString(t->data()); return t->data(); } else { qCDebug(KLEOPATRA_LOG) << "gpgagent_data(" << command << "): t == NULL"; return std::string(); } } static const std::vector< std::pair > gpgagent_statuslines(std::shared_ptr gpgAgent, const char *what, Error &err) { const std::unique_ptr t = gpgagent_default_transact(gpgAgent, what, err); if (t.get()) { qCDebug(KLEOPATRA_LOG) << "agent_getattr_status(" << what << "): got" << t->statusLines(); return t->statusLines(); } else { qCDebug(KLEOPATRA_LOG) << "agent_getattr_status(" << what << "): t == NULL"; return std::vector >(); } } static const std::string gpgagent_status(const std::shared_ptr &gpgAgent, const char *what, Error &err) { const auto lines = gpgagent_statuslines (gpgAgent, what, err); // The status is only the last attribute // e.g. for SCD SERIALNO it would only be "SERIALNO" and for SCD GETATTR FOO // it would only be FOO const char *p = strrchr(what, ' '); const char *needle = (p + 1) ? (p + 1) : what; for (const auto &pair: lines) { if (pair.first == needle) { return pair.second; } } return std::string(); } static const std::string scd_getattr_status(std::shared_ptr &gpgAgent, const char *what, Error &err) { std::string cmd = "SCD GETATTR "; cmd += what; return gpgagent_status(gpgAgent, cmd.c_str(), err); } +static std::vector getCardsAndApps(std::shared_ptr &gpgAgent, Error &err) +{ + std::vector result; + const std::string command = "SCD GETINFO all_active_apps"; + const auto statusLines = gpgagent_statuslines(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); + // 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); + } + } + 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 = gpgagent_statuslines(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 = gpgagent_statuslines(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 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 const std::string get_manufacturer(std::shared_ptr &gpgAgent, Error &err) { // The result of SCD GETATTR MANUFACTURER is the manufacturer ID as unsigned number // optionally followed by the name of the manufacturer, e.g. // 6 Yubico // 65534 unmanaged S/N range const auto manufacturerIdAndName = scd_getattr_status(gpgAgent, "MANUFACTURER", err); if (err.code()) { if (err.code() == GPG_ERR_INV_NAME) { qCDebug(KLEOPATRA_LOG) << "get_manufacturer(): Querying for attribute MANUFACTURER not yet supported; needs GnuPG 2.2.21+"; } else { qCDebug(KLEOPATRA_LOG) << "get_manufacturer(): GpgME::Error(" << err.encodedError() << " (" << err.asString() << "))"; } return std::string(); } const auto startOfManufacturerName = manufacturerIdAndName.find(' '); if (startOfManufacturerName == std::string::npos) { // only ID without manufacturer name return "unknown"; } const auto manufacturerName = manufacturerIdAndName.substr(startOfManufacturerName + 1); return manufacturerName; } static void handle_openpgp_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto ret = new OpenPGPCard(); ret->setSerialNumber(ci->serialNumber()); ret->setManufacturer(get_manufacturer(gpg_agent, err)); if (err.code()) { // fallback, e.g. if gpg does not yet support querying for the MANUFACTURER attribute ret->setManufacturer(get_openpgp_card_manufacturer_from_serial_number(ci->serialNumber())); } const auto info = gpgagent_statuslines(gpg_agent, "SCD LEARN --keypairinfo", err); if (err.code()) { ci->setStatus(Card::CardError); return; } ret->setKeyPairInfo(info); ci.reset(ret); } 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 = gpgagent_statuslines(gpg_agent, command.c_str(), err); if (err) { qCWarning(KLEOPATRA_LOG) << "readKeyPairInfoFromPIVCard(): Error on " << QString::fromStdString(command) << ":" << "GpgME::Error(" << err.encodedError() << " (" << err.asString() << "))"; return; } for (const auto &pair: keyPairInfoLines) { if (pair.first == "KEYPAIRINFO") { const KeyPairInfo info = KeyPairInfo::fromStatusLine(pair.second); if (info.grip.empty()) { qCWarning(KLEOPATRA_LOG) << "Invalid KEYPAIRINFO status line" << QString::fromStdString(pair.second); continue; } pivCard->setKeyAlgorithm(keyRef, info.algorithm); } else { - qCWarning(KLEOPATRA_LOG) << "readKeyPairInfoFromPIVCard(): Unexpected status line on " - << QString::fromStdString(command) << ":" << QString::fromStdString(pair.first) - << QString::fromStdString(pair.second); + logUnexpectedStatusLine(pair, "readKeyPairInfoFromPIVCard()", command); } } } 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 = gpgagent_data(gpg_agent, command.c_str(), err); if (err && err.code() != GPG_ERR_NOT_FOUND) { qCWarning(KLEOPATRA_LOG) << "readCertificateFromPIVCard(" << QString::fromStdString(keyRef) << "): Error on " << QString::fromStdString(command) << ":" << "GpgME::Error(" << err.encodedError() << " (" << err.asString() << "))"; 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(); pivCard->setSerialNumber(ci->serialNumber()); const auto displaySerialnumber = scd_getattr_status(gpg_agent, "$DISPSERIALNO", err); if (err) { qCWarning(KLEOPATRA_LOG) << "handle_piv_card(): Error on GETATTR $DISPSERIALNO:" << "GpgME::Error(" << err.encodedError() << " (" << err.asString() << "))"; } pivCard->setDisplaySerialNumber(err ? ci->serialNumber() : displaySerialnumber); const auto info = gpgagent_statuslines(gpg_agent, "SCD LEARN --force", err); if (err) { ci->setStatus(Card::CardError); return; } pivCard->setCardInfo(info); for (const std::string &keyRef : PIVCard::supportedKeys()) { if (!pivCard->keyGrip(keyRef).empty()) { readKeyPairInfoFromPIVCard(keyRef, pivCard, gpg_agent); readCertificateFromPIVCard(keyRef, pivCard, gpg_agent); } } ci.reset(pivCard); } static void handle_netkey_card(std::shared_ptr &ci, std::shared_ptr &gpg_agent) { Error err; auto nkCard = new NetKeyCard(); nkCard->setSerialNumber(ci->serialNumber()); ci.reset(nkCard); ci->setAppVersion(parse_app_version(scd_getattr_status(gpg_agent, "NKS-VERSION", err))); if (err.code()) { qCDebug(KLEOPATRA_LOG) << "NKS-VERSION resulted in error" << err.asString(); ci->setErrorMsg(QStringLiteral ("NKS-VERSION failed: ") + QString::fromUtf8(err.asString())); return; } if (ci->appVersion() != 3) { qCDebug(KLEOPATRA_LOG) << "not a NetKey v3 card, giving up. Version:" << ci->appVersion(); ci->setErrorMsg(QStringLiteral("NetKey v%1 cards are not supported.").arg(ci->appVersion())); return; } // 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) << "no CHV-STATUS" << err.asString(); ci->setErrorMsg(QStringLiteral ("CHV-Status failed: ") + QString::fromUtf8(err.asString())); 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); // check for keys to learn: const std::unique_ptr result = gpgagent_default_transact(gpg_agent, "SCD LEARN --keypairinfo", err); if (err.code() || !result.get()) { if (err) { ci->setErrorMsg(QString::fromLatin1(err.asString())); } else { ci->setErrorMsg(QStringLiteral("Invalid internal state. No result.")); } return; } const std::vector keyPairInfos = result->statusLine("KEYPAIRINFO"); if (keyPairInfos.empty()) { return; } nkCard->setKeyPairInfo(keyPairInfos); } -static std::shared_ptr get_card_status(unsigned int slot, std::shared_ptr &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(" << slot << ',' << gpg_agent.get() << ')'; - auto ci = std::shared_ptr (new Card()); - if (slot != 0 || !gpg_agent) { - // In the future scdaemon should support multiple slots but - // not yet (2.1.18) - return ci; - } + qCDebug(KLEOPATRA_LOG) << "get_card_status(" << serialNumber << ',' << appName << ',' << gpg_agent.get() << ')'; + auto ci = std::shared_ptr(new Card()); - Error err; - ci->setSerialNumber(gpgagent_status(gpg_agent, "SCD SERIALNO", err)); - if (err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED) { - ci->setStatus(Card::NoCard); - return ci; + // 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); } - if (err.code()) { - ci->setStatus(Card::CardError); - return ci; + + // 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->setStatus(Card::CardPresent); - const auto appName = scd_getattr_status(gpg_agent, "APPTYPE", err); + ci->setSerialNumber(serialNumber); ci->setAppName(appName); - if (err.code()) { - return ci; - } // Handle different card types if (ci->appName() == NetKeyCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found Netkey card" << ci->serialNumber().c_str() << "end"; handle_netkey_card(ci, gpg_agent); return ci; } else if (ci->appName() == OpenPGPCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found OpenPGP card" << ci->serialNumber().c_str() << "end"; handle_openpgp_card(ci, gpg_agent); return ci; } else if (ci->appName() == PIVCard::AppName) { qCDebug(KLEOPATRA_LOG) << "get_card_status: found PIV card" << ci->serialNumber().c_str() << "end"; handle_piv_card(ci, gpg_agent); return ci; } else { - qCDebug(KLEOPATRA_LOG) << "get_card_status: unhandled application:" << appName.c_str(); + qCDebug(KLEOPATRA_LOG) << "get_card_status: unhandled application:" << appName; return ci; } return ci; } static std::vector > update_cardinfo(std::shared_ptr &gpgAgent) { - // Multiple smartcard readers are only supported internally by gnupg - // but not by scdaemon (Status gnupg 2.1.18) - // We still pretend that there can be multiple cards inserted - // at once but we don't handle it yet. - const auto ci = get_card_status(0, gpgAgent); - return std::vector >(1, ci); + qCDebug(KLEOPATRA_LOG) << "update_cardinfo()"; + + // ensure that a card is present and that all cards are properly set up + { + Error err; + const std::string serialno = gpgagent_status(gpgAgent, "SCD SERIALNO --all", err); + if (err) { + auto ci = std::shared_ptr(new Card()); + if (err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED) { + ci->setStatus(Card::NoCard); + } else { + ci->setStatus(Card::CardError); + } + return std::vector >(1, ci); + } + } + + Error err; + const std::vector cardApps = getCardsAndApps(gpgAgent, err); + if (err) { + auto ci = std::shared_ptr(new Card()); + if (err.code() == GPG_ERR_CARD_NOT_PRESENT || err.code() == GPG_ERR_CARD_REMOVED) { + ci->setStatus(Card::NoCard); + } else { + 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; const char *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 cardChanged(unsigned int); void oneTransactionFinished(const GpgME::Error &err); public Q_SLOTS: 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); Q_FOREACH (const Transaction &t, ft) if (t.receiver && t.slot && *t.slot) { QMetaObject::invokeMethod(t.receiver, t.slot, Qt::DirectConnection, Q_ARG(GpgME::Error, err)); } } 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; 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)) { std::vector > newCards = update_cardinfo(gpgAgent); - newCards.resize(std::max(newCards.size(), oldCards.size())); - oldCards.resize(std::max(newCards.size(), oldCards.size())); - KDAB_SYNCHRONIZED(m_mutex) m_cardInfos = newCards; std::vector >::const_iterator nit = newCards.begin(), nend = newCards.end(), oit = oldCards.begin(), oend = oldCards.end(); unsigned int idx = 0; bool anyLC = false; std::string firstCardWithNullPin; bool anyError = false; - while (nit != nend && oit != oend) { - const auto optr = (*oit).get(); - const auto nptr = (*nit).get(); + while (nit != nend || oit != oend) { + const Card *optr = oit != oend ? (*oit).get() : nullptr; + const Card *nptr = nit != nend ? (*nit).get() : nullptr; if ((optr && !nptr) || (!optr && nptr) || (optr && nptr && *optr != *nptr)) { qCDebug(KLEOPATRA_LOG) << "ReaderStatusThread[2nd]: slot" << idx << ": card Changed"; Q_EMIT cardChanged(idx); } - if ((*nit)->canLearnKeys()) { - anyLC = true; + if (nptr) { + if (nptr->canLearnKeys()) { + anyLC = true; + } + if (nptr->hasNullPin() && firstCardWithNullPin.empty()) { + firstCardWithNullPin = (*nit)->serialNumber(); + } + if (nptr->status() == Card::CardError) { + anyError = true; + } } - if ((*nit)->hasNullPin() && firstCardWithNullPin.empty()) { - firstCardWithNullPin = (*nit)->serialNumber(); + if (nit != nend) { + ++nit; } - if ((*nit)->status() == Card::CardError) { - anyError = true; + if (oit != oend) { + ++oit; } - ++nit; - ++oit; ++idx; } Q_EMIT firstCardWithNullPinChanged(firstCardWithNullPin); Q_EMIT anyCardCanLearnKeysChanged(anyLC); if (anyError) { gpgAgent.reset(); } } else { GpgME::Error err; switchCard(gpgAgent, cardApp.serialNumber, err); if (!err) { switchApp(gpgAgent, cardApp.serialNumber, cardApp.appName, err); } if (!err) { if (assuanTransaction) { (void)gpgagent_transact(gpgAgent, command.constData(), std::unique_ptr(assuanTransaction), err); } else { (void)gpgagent_default_transact(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"); watcher.whitelistFiles(QStringList(QStringLiteral("reader_*.status"))); watcher.addPath(Kleo::gnupgHomeDirectory()); watcher.setDelay(100); connect(this, &::ReaderStatusThread::cardChanged, q, &ReaderStatus::cardChanged); connect(this, &::ReaderStatusThread::firstCardWithNullPinChanged, q, &ReaderStatus::firstCardWithNullPinChanged); connect(this, &::ReaderStatusThread::anyCardCanLearnKeysChanged, q, &ReaderStatus::anyCardCanLearnKeysChanged); connect(&watcher, &FileSystemWatcher::triggered, this, &::ReaderStatusThread::ping); } ~Private() { 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; }; 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(); } // 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 char *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 char *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(); } std::vector > ReaderStatus::getCards() const { return d->cardInfos(); } #include "readerstatus.moc" diff --git a/src/view/smartcardwidget.cpp b/src/view/smartcardwidget.cpp index 44b70b487..68ae44530 100644 --- a/src/view/smartcardwidget.cpp +++ b/src/view/smartcardwidget.cpp @@ -1,150 +1,153 @@ /* view/smartcardwidget.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-License-Identifier: GPL-2.0-or-later */ #include "smartcardwidget.h" #include "smartcard/readerstatus.h" #include "smartcard/openpgpcard.h" #include "smartcard/netkeycard.h" #include "smartcard/pivcard.h" #include "view/pgpcardwidget.h" #include "view/netkeywidget.h" #include "view/pivcardwidget.h" #include "kleopatra_debug.h" #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::SmartCard; namespace { class PlaceHolderWidget: public QWidget { Q_OBJECT public: explicit PlaceHolderWidget(QWidget *parent = nullptr) : QWidget(parent) { auto lay = new QVBoxLayout; lay->addStretch(-1); const QStringList supported = QStringList() << QStringLiteral("OpenPGP v2.0 - v3.3") << QStringLiteral("Gnuk") << QStringLiteral("NetKey v3") << QStringLiteral("PIV"); lay->addWidget(new QLabel(QStringLiteral("\t\t

") + i18n("Please insert a compatible smartcard.") + QStringLiteral("

"), this)); lay->addSpacing(10); lay->addWidget(new QLabel(QStringLiteral("\t\t") + i18n("Kleopatra currently supports the following card types:") + QStringLiteral("
  • ") + supported.join(QLatin1String("
  • ")) + QStringLiteral("
"), this)); lay->addSpacing(10); lay->addWidget(new QLabel(i18n("Refresh the view (F5) to update the smartcard status."), this)); lay->addStretch(-1); auto hLay = new QHBoxLayout(this); hLay->addStretch(-1); hLay->addLayout(lay); hLay->addStretch(-1); lay->addStretch(-1); } }; } // namespace class SmartCardWidget::Private { public: Private(SmartCardWidget *qq) : q(qq) { QPushButton *backBtn = new QPushButton(QIcon::fromTheme(QStringLiteral("arrow-left")), i18n("Back")); QHBoxLayout *backH = new QHBoxLayout; backH->addWidget(backBtn); backH->addWidget(new QLabel(QStringLiteral("

") + i18n("Smartcard Management") + QStringLiteral("

"))); backH->addStretch(-1); QVBoxLayout *vLay = new QVBoxLayout(q); connect(backBtn, &QPushButton::clicked, q, [this] () {Q_EMIT q->backRequested();}); vLay->addLayout(backH); mStack = new QStackedWidget; vLay->addWidget(mStack); mPGPCardWidget = new PGPCardWidget(q); mStack->addWidget(mPGPCardWidget); mNetKeyWidget = new NetKeyWidget(q); mStack->addWidget(mNetKeyWidget); mPIVCardWidget = new PIVCardWidget(q); mStack->addWidget(mPIVCardWidget); mPlaceHolderWidget = new PlaceHolderWidget(q); mStack->addWidget(mPlaceHolderWidget); mStack->setCurrentWidget(mPlaceHolderWidget); - connect (ReaderStatus::instance(), &ReaderStatus::cardChanged, q, [this] (unsigned int /*slot*/) { - const auto cards = ReaderStatus::instance()->getCards(); - if (!cards.size()) { - setCard(std::shared_ptr(new Card())); - } else { - // No support for multiple reader / cards currently - setCard(cards[0]); - } - }); + connect(ReaderStatus::instance(), &ReaderStatus::cardChanged, + q, [this] (unsigned int slot) { + if (slot == 0) { + const auto cards = ReaderStatus::instance()->getCards(); + if (!cards.size()) { + setCard(std::shared_ptr(new Card())); + } else { + // No support for multiple reader / cards currently + setCard(cards[0]); + } + } + }); } void setCard(std::shared_ptr card) { if (card->appName() == SmartCard::OpenPGPCard::AppName) { mPGPCardWidget->setCard(static_cast (card.get())); mStack->setCurrentWidget(mPGPCardWidget); } else if (card->appName() == SmartCard::NetKeyCard::AppName) { mNetKeyWidget->setCard(static_cast (card.get())); mStack->setCurrentWidget(mNetKeyWidget); } else if (card->appName() == SmartCard::PIVCard::AppName) { mPIVCardWidget->setCard(static_cast (card.get())); mStack->setCurrentWidget(mPIVCardWidget); } else { mStack->setCurrentWidget(mPlaceHolderWidget); } } private: SmartCardWidget *const q; NetKeyWidget *mNetKeyWidget; PGPCardWidget *mPGPCardWidget; PIVCardWidget *mPIVCardWidget; PlaceHolderWidget *mPlaceHolderWidget; QStackedWidget *mStack; }; SmartCardWidget::SmartCardWidget(QWidget *parent): QWidget(parent), d(new Private(this)) { } void SmartCardWidget::reload() { ReaderStatus::mutableInstance()->updateStatus(); } #include "smartcardwidget.moc"