/** * Copyright (C) 2021-2024 Saturneric * * This file is part of GpgFrontend. * * GpgFrontend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GpgFrontend is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GpgFrontend. If not, see . * * The initial version of the source code is inherited from * the gpg4usb project, which is under GPL-3.0-or-later. * * All the source code of GpgFrontend was modified and released by * Saturneric starting on May 12, 2021. * * SPDX-License-Identifier: GPL-3.0-or-later * */ #include "SmartCardControllerDialog.h" #include "core/function/gpg/GpgAdvancedOperator.h" #include "core/function/gpg/GpgCommandExecutor.h" #include "core/function/gpg/GpgSmartCardManager.h" #include "core/utils/GpgUtils.h" #include "ui/UISignalStation.h" #include "ui/UserInterfaceUtils.h" #include "ui/dialog/key_generate/GenerateCardKeyDialog.h" // #include "ui_SmartCardControllerDialog.h" namespace GpgFrontend::UI { SmartCardControllerDialog::SmartCardControllerDialog(QWidget* parent) : GeneralDialog("SmartCardControllerDialog", parent), ui_(QSharedPointer::create()), channel_(kGpgFrontendDefaultChannel), scd_version_supported_( GpgSmartCardManager::GetInstance(channel_).IsSCDVersionSupported()) { ui_->setupUi(this); ui_->smartCardLabel->setText(tr("Smart Card(s):")); ui_->keyStubLabel->setText(tr("Key Stub(s) in Key Database(s):")); ui_->cNameButton->setText(tr("Change Name")); ui_->cLangButton->setText(tr("Change Language")); ui_->cGenderButton->setText(tr("Change Gender")); ui_->cLoginDataButton->setText(tr("Change Login Data")); ui_->cPubKeyURLButton->setText(tr("Change Public Key URL")); ui_->cPINButton->setText(tr("Change PIN")); ui_->cAdminPINButton->setText(tr("Change Admin PIN")); ui_->cResetCodeButton->setText(tr("Change Reset Code")); ui_->fetchButton->setText(tr("Fetch")); ui_->restartGpgAgentButton->setText(tr("Restart All Gpg-Agents")); ui_->generateKeysButton->setText(tr("Generate Card Keys")); ui_->refreshButton->setText(tr("Refresh")); ui_->operationGroupBox->setTitle(tr("Operations")); for (const auto& key_db : GetGpgKeyDatabaseInfos()) { ui_->keyDBIndexComboBox->insertItem( key_db.channel, QString("%1: %2").arg(key_db.channel).arg(key_db.name)); } connect(ui_->keyDBIndexComboBox, qOverload(&QComboBox::currentIndexChanged), this, [=](int index) { refresh_key_tree_view(index); }); connect(ui_->keyDBIndexComboBox, &QComboBox::currentTextChanged, this, [=](const QString& serial_number) { select_smart_card_by_serial_number(serial_number); }); connect(ui_->refreshButton, &QPushButton::clicked, this, [=](bool) { slot_refresh(); }); connect(ui_->fetchButton, &QPushButton::clicked, this, [=](bool) { slot_fetch_smart_card_keys(); }); connect(ui_->cNameButton, &QPushButton::clicked, this, [=](bool) { modify_key_attribute("DISP-NAME"); }); connect(ui_->cLangButton, &QPushButton::clicked, this, [=](bool) { modify_key_attribute("DISP-LANG"); }); connect(ui_->cGenderButton, &QPushButton::clicked, this, [=](bool) { modify_key_attribute("DISP-SEX"); }); connect(ui_->cPubKeyURLButton, &QPushButton::clicked, this, [=](bool) { modify_key_attribute("PUBKEY-URL"); }); connect(ui_->cLoginDataButton, &QPushButton::clicked, this, [=](bool) { modify_key_attribute("LOGIN-DATA"); }); connect(ui_->cPINButton, &QPushButton::clicked, this, [=](bool) { modify_key_pin("OPENPGP.1"); }); connect(ui_->cAdminPINButton, &QPushButton::clicked, this, [=](bool) { modify_key_pin("OPENPGP.3"); }); connect(ui_->cResetCodeButton, &QPushButton::clicked, this, [=](bool) { modify_key_pin("OPENPGP.2"); }); connect(ui_->restartGpgAgentButton, &QPushButton::clicked, this, [=](bool) { bool ret = true; const auto size = GpgContext::GetAllChannelId().size(); for (auto i = 0; i < size; i++) { ret = GpgAdvancedOperator::GetInstance().RestartGpgComponents(); if (!ret) break; } if (ret) { QMessageBox::information( this, tr("Successful Operation"), tr("Restart all the GnuPG's components successfully")); } else { QMessageBox::critical( this, tr("Failed Operation"), tr("Failed to restart all or one of the GnuPG's component(s)")); } }); connect(ui_->generateKeysButton, &QPushButton::clicked, this, [=](bool) { auto serial_number = ui_->currentCardComboBox->currentText(); auto* d = new GenerateCardKeyDialog(channel_, serial_number, this); connect(d, &GenerateCardKeyDialog::finished, this, [=](int ret) { if (ret == 1) { fetch_smart_card_info(serial_number); } else { QMessageBox::critical(this, tr("Error"), tr("Generate card key failed.")); } }); }); connect(UISignalStation::GetInstance(), &UISignalStation::SignalKeyDatabaseRefreshDone, this, [=]() { refresh_key_tree_view(ui_->keyDBIndexComboBox->currentIndex()); }); // instant refresh slot_listen_smart_card_changes(); timer_ = new QTimer(this); connect(timer_, &QTimer::timeout, this, &SmartCardControllerDialog::slot_listen_smart_card_changes); if (scd_version_supported_) { timer_->start(3000); } setWindowTitle(tr("Smart Card Controller")); } void SmartCardControllerDialog::select_smart_card_by_serial_number( const QString& serial_number) { if (serial_number.isEmpty()) { reset_status(); return; } auto [err, status] = GpgSmartCardManager::GetInstance(channel_).SelectCardBySerialNumber( serial_number); if (err != GPG_ERR_NO_ERROR) { LOG_E() << "select card by serial number failed, err:" << CheckGpgError(err) << "status:" << status; CommonUtils::RaiseFailureMessageBox(this, err, status); reset_status(); return; } LOG_D() << "selected smart card by serial number: " << serial_number; has_card_ = true; fetch_smart_card_info(serial_number); } void SmartCardControllerDialog::fetch_smart_card_info( const QString& serial_number) { if (!has_card_) return; reset_status(); auto card_info = GpgSmartCardManager::GetInstance(channel_).FetchCardInfoBySerialNumber( serial_number); if (card_info == nullptr) { LOG_E() << "card info is nullptr, serial number:" << serial_number; reset_status(); return; } card_info_ = *card_info; has_card_ = true; print_smart_card_info(); slot_disable_controllers(!has_card_); refresh_key_tree_view(ui_->keyDBIndexComboBox->currentIndex()); } void SmartCardControllerDialog::print_smart_card_info() { if (!has_card_) return; QString html; QTextStream out(&html); const auto& card = card_info_; out << "

" << tr("OpenPGP Card Information") << "

"; out << "

" << tr("Basic Information") << "

    "; out << "
  • " << tr("Reader") << ":" << " " << card.reader << "
  • "; out << "
  • " << tr("Serial Number") << ":" << " " << card.serial_number << "
  • "; out << "
  • " << tr("Card Type") << ":" << " " << card.card_type << "
  • "; out << "
  • " << tr("Card Version") << ":" << " " << card.card_version << "
  • "; out << "
  • " << tr("App Type") << ":" << " " << card.app_type << "
  • "; out << "
  • " << tr("App Version") << ":" << " " << card.app_version << "
  • "; out << "
  • " << tr("Manufacturer ID") << ":" << " " << card.manufacturer_id << "
  • "; out << "
  • " << tr("Manufacturer") << ":" << " " << card.manufacturer << "
  • "; out << "
  • " << tr("Card Holder") << ":" << " " << card.card_holder << "
  • "; out << "
  • " << tr("Language") << ":" << " " << card.display_language << "
  • "; out << "
  • " << tr("Sex") << ":" << " " << card.display_sex << "
  • "; out << "
"; out << "

" << tr("Status") << "

    "; out << "
  • " << tr("Signature Counter") << ":" << " " << card.sig_counter << "
  • "; out << "
  • " << tr("CHV1 Cached") << ":" << " " << card.chv1_cached << "
  • "; out << "
  • " << tr("CHV Max Length") << ":" << " " << QString("%1, %2, %3") .arg(card.chv_max_len[0]) .arg(card.chv_max_len[1]) .arg(card.chv_max_len[2]) << "
  • "; out << "
  • " << tr("CHV Retry Left") << ":" << " " << QString("%1, %2, %3") .arg(card.chv_retry[0]) .arg(card.chv_retry[1]) .arg(card.chv_retry[2]) << "
  • "; out << "
  • " << tr("KDF Status") << ":" << " "; switch (card.kdf_do_enabled) { case 0: out << tr("Not enabled"); break; case 1: out << tr("Enabled (no protection)"); break; case 2: out << tr("Enabled with salt protection"); break; default: out << tr("Unknown"); break; } out << "
  • "; out << "
  • " << tr("UIF") << ":" << "
      "; out << "
    • " << tr("Sign") << ":" << (card.uif.sign ? tr("Enabled") : tr("Disabled")) << "
    • "; out << "
    • " << tr("Encrypt") << ":" << (card.uif.encrypt ? tr("Enabled") : tr("Disabled")) << "
    • "; out << "
    • " << tr("Authenticate") << ":" << (card.uif.auth ? tr("Enabled") : tr("Disabled")) << "
    • "; out << "
  • "; out << "
"; out << "

" << tr("Key Information") << "

"; out << "
"; if (card.card_keys_info.isEmpty()) { out << "" << tr("No key information available.") << ""; } else { out << ""; out << ""; for (auto it = card.card_keys_info.begin(); it != card.card_keys_info.end(); ++it) { const auto& info = it.value(); out << ""; } out << "
" << tr("No.") << "" << tr("Fingerprint") << "" << tr("Created") << "" << tr("Grip") << "" << tr("Type") << "" << tr("Algorithm") << "" << tr("Usage") << "" << tr("Curve") << "
" << it.key() << "" << info.fingerprint << "" << info.created.toString(Qt::ISODate) << "" << info.grip << "" << info.key_type << "" << info.algo << "" << info.usage << "" << info.algo << "
"; } out << "
"; out << "

" << tr("Extended Capabilities") << "

    "; out << "
  • " << tr("Key Info (ki): %1").arg(card.ext_cap.ki ? tr("Yes") : tr("No")) << "
  • "; out << "
  • " << tr("Additional Auth (aac): %1") .arg(card.ext_cap.aac ? tr("Yes") : tr("No")) << "
  • "; out << "
  • " << tr("Biometric Terminal (bt): %1") .arg(card.ext_cap.bt ? tr("Yes") : tr("No")) << "
  • "; out << "
  • " << tr("KDF Supported: %1").arg(card.ext_cap.kdf ? tr("Yes") : tr("No")) << "
  • "; out << "
  • " << tr("Status Indicator") << QString(": %1").arg(card.ext_cap.status_indicator) << "
  • "; out << "
"; if (!card.additional_card_infos.isEmpty()) { out << "

" << tr("Additional Info") << "

    "; for (auto it = card.additional_card_infos.begin(); it != card.additional_card_infos.end(); ++it) { out << "
  • " << QString("%1:").arg(it.key()) << " " << it.value() << "
  • "; } out << "
"; } ui_->cardInfoEdit->setText(html); } void SmartCardControllerDialog::slot_refresh() { scd_version_supported_ = GpgSmartCardManager::GetInstance(channel_).IsSCDVersionSupported(); if (scd_version_supported_ && !timer_->isActive()) { timer_->start(3000); } fetch_smart_card_info(ui_->currentCardComboBox->currentText()); } void SmartCardControllerDialog::refresh_key_tree_view(int channel) { if (!has_card_) return; ui_->cardKeysTreeView->SetChannel(channel); QStringList card_fprs; for (const auto& key_info : card_info_.card_keys_info.values()) { card_fprs.append(key_info.fingerprint); } LOG_D() << "card key fingerprints:" << card_fprs; if (card_fprs.isEmpty()) { ui_->cardKeysTreeView->SetKeyFilter([](auto) { return false; }); return; } ui_->cardKeysTreeView->SetKeyFilter([=](const GpgAbstractKey* k) { return card_fprs.contains(k->Fingerprint()); }); ui_->cardKeysTreeView->expandAll(); } void SmartCardControllerDialog::reset_status() { has_card_ = false; ui_->cardInfoEdit->clear(); slot_disable_controllers(true); card_info_ = GpgOpenPGPCard(); QString html; QTextStream out(&html); out << "

" << tr("No OpenPGP Smart Card Found") << "

"; out << "

" << tr("No OpenPGP-compatible smart card has been detected.") << "

"; out << "

" << tr("An OpenPGP Smart Card is a physical device that securely " "stores your private cryptographic keys and can be used for " "digital signing, encryption, and authentication. Popular " "examples include YubiKey, Nitrokey, and other " "GnuPG-compatible tokens.") << "

"; out << "

" << tr("Make sure your card is inserted and properly recognized by " "the system. You can also try reconnecting the card or " "restarting the application.") << "

"; out << "

" << tr("Note: Smart card support of GpgFrontend requires GnuPG version " "2.3.0 or later.") << "

"; out << "

" << tr("Read the GnuPG Smart Card HOWTO: ") << "https://gnupg.org/howtos/card-howto/en/" << "

"; ui_->cardInfoEdit->setText(html); } void SmartCardControllerDialog::slot_listen_smart_card_changes() { if (!scd_version_supported_) { LOG_D() << "scd version is not suppored"; reset_status(); return; } auto serial_numbers = GpgSmartCardManager::GetInstance(channel_).GetSerialNumbers(); const auto hash = QCryptographicHash::hash(serial_numbers.join(' ').toUtf8(), QCryptographicHash::Sha1) .toHex(); // check and skip if (cached_status_hash_ == hash) return; ui_->currentCardComboBox->clear(); if (serial_numbers.isEmpty()) { LOG_D() << "no inserted and supported smart card found."; reset_status(); return; } int index = 0; for (const auto& serial_number : serial_numbers) { ui_->currentCardComboBox->insertItem(index++, serial_number); } cached_status_hash_ = hash; ui_->currentCardComboBox->setCurrentIndex(0); select_smart_card_by_serial_number(ui_->currentCardComboBox->currentText()); } void SmartCardControllerDialog::slot_disable_controllers(bool disable) { ui_->operationGroupBox->setDisabled(disable); ui_->keyDBIndexComboBox->setDisabled(disable); ui_->cardKeysTreeView->setDisabled(disable); } void SmartCardControllerDialog::slot_fetch_smart_card_keys() { ui_->fetchButton->setDisabled(true); auto err = GpgSmartCardManager::GetInstance().Fetch( ui_->currentCardComboBox->currentText()); if (err != GPG_ERR_NO_ERROR) { CommonUtils::RaiseFailureMessageBox(this, err); return; } QTimer::singleShot(1000, [=]() { GpgCommandExecutor::GetInstance(channel_).GpgExecuteSync( {{}, {"--card-status"}, [=](int exit_code, const QString&, const QString&) { ui_->fetchButton->setDisabled(false); LOG_D() << "gpg --card--status exit code: " << exit_code; if (exit_code != 0) return; emit UISignalStation::GetInstance() -> SignalKeyDatabaseRefresh(); }}); }); } auto AskIsoDisplayName(QWidget* parent, bool* ok) -> QString { QString surname = QInputDialog::getText( parent, SmartCardControllerDialog::tr("Cardholder's Surname"), SmartCardControllerDialog::tr("Please enter your surname (e.g., Lee):"), QLineEdit::Normal, "", ok); if (!*ok || surname.trimmed().isEmpty()) return {}; QString given_name = QInputDialog::getText( parent, SmartCardControllerDialog::tr("Cardholder's Given Name"), SmartCardControllerDialog::tr( "Please enter your given name (e.g., Chris):"), QLineEdit::Normal, "", ok); if (!*ok || given_name.trimmed().isEmpty()) return {}; QString iso_name = surname.trimmed() + "<<" + given_name.trimmed(); iso_name.replace(" ", "<"); if (iso_name.length() > 39) { QMessageBox::warning(parent, SmartCardControllerDialog::tr("Too Long"), SmartCardControllerDialog::tr( "Combined name too long (max 39 characters).")); *ok = false; return {}; } return iso_name; } void SmartCardControllerDialog::modify_key_attribute(const QString& attr) { QString value; bool ok = false; if (attr == "DISP-SEX") { QStringList options; options << "1 - " + tr("Male") << "2 - " + tr("Female"); const QString selected = QInputDialog::getItem( this, tr("Modify Card Attribute"), tr("Select sex to store in '%1'").arg(attr) + ": ", options, 0, false, &ok); if (!ok || selected.isEmpty()) return; value = selected.left(1); } else if (attr == "DISP-NAME") { value = AskIsoDisplayName(this, &ok); if (!ok || value.trimmed().isEmpty()) { LOG_D() << "user canceled or empty input."; return; } } else { value = QInputDialog::getText( this, tr("Modify Card Attribute"), tr("Enter new value for attribute '%1'").arg(attr) + ": ", QLineEdit::Normal, "", &ok); if (!ok || value.isEmpty()) { LOG_D() << "user canceled or empty input."; return; } } auto [err, status] = GpgSmartCardManager::GetInstance(channel_).ModifyAttr(attr, value); if (err != GPG_ERR_NO_ERROR) { LOG_D() << "SCD SETATTR command failed for attr:" << attr << ", err:" << CheckGpgError(err); CommonUtils::RaiseFailureMessageBox(this, err, status); return; } QMessageBox::information(this, tr("Success"), tr("Attribute operation completed successfully.")); fetch_smart_card_info(ui_->currentCardComboBox->currentText()); } void SmartCardControllerDialog::modify_key_pin(const QString& pinref) { auto [err, status] = GpgSmartCardManager::GetInstance(channel_).ModifyPin(pinref); if (err != GPG_ERR_NO_ERROR) { CommonUtils::RaiseFailureMessageBox(this, err, status); return; } QMessageBox::information(this, tr("Success"), tr("PIN operation completed successfully.")); fetch_smart_card_info(ui_->currentCardComboBox->currentText()); } } // namespace GpgFrontend::UI