aboutsummaryrefslogtreecommitdiffstats
path: root/src/ui
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2025-04-13 19:33:31 +0000
committersaturneric <[email protected]>2025-04-13 19:33:31 +0000
commit7ca18eb0e2c4204f749c682b66c862968e8d2f58 (patch)
treeba8e0303ebc6ec376b1be580408d9cd91f58f2d8 /src/ui
parentfeat: add openpgp smart card support (diff)
downloadGpgFrontend-7ca18eb0e2c4204f749c682b66c862968e8d2f58.tar.gz
GpgFrontend-7ca18eb0e2c4204f749c682b66c862968e8d2f58.zip
feat: add SmartCardController
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/dialog/GeneralDialog.cpp3
-rw-r--r--src/ui/dialog/controller/ModuleControllerDialog.cpp2
-rw-r--r--src/ui/dialog/controller/ModuleControllerDialog.h3
-rw-r--r--src/ui/dialog/controller/SmartCardControllerDialog.cpp565
-rw-r--r--src/ui/dialog/controller/SmartCardControllerDialog.h50
-rw-r--r--src/ui/main_window/GeneralMainWindow.cpp3
-rw-r--r--src/ui/widgets/KeyTreeView.cpp10
7 files changed, 588 insertions, 48 deletions
diff --git a/src/ui/dialog/GeneralDialog.cpp b/src/ui/dialog/GeneralDialog.cpp
index 7f9a9f92..2b0fcb3d 100644
--- a/src/ui/dialog/GeneralDialog.cpp
+++ b/src/ui/dialog/GeneralDialog.cpp
@@ -37,6 +37,9 @@ GpgFrontend::UI::GeneralDialog::GeneralDialog(QString name, QWidget *parent)
: QDialog(parent), name_(std::move(name)) {
slot_restore_settings();
connect(this, &QDialog::finished, this, &GeneralDialog::slot_save_settings);
+
+ // should delete itself at closing by default
+ setAttribute(Qt::WA_DeleteOnClose);
}
GpgFrontend::UI::GeneralDialog::~GeneralDialog() = default;
diff --git a/src/ui/dialog/controller/ModuleControllerDialog.cpp b/src/ui/dialog/controller/ModuleControllerDialog.cpp
index 2b842c7f..2ad13151 100644
--- a/src/ui/dialog/controller/ModuleControllerDialog.cpp
+++ b/src/ui/dialog/controller/ModuleControllerDialog.cpp
@@ -40,7 +40,7 @@
namespace GpgFrontend::UI {
ModuleControllerDialog::ModuleControllerDialog(QWidget* parent)
- : QDialog(parent),
+ : GeneralDialog("ModuleControllerDialog", parent),
ui_(std::make_shared<Ui_ModuleControllerDialog>()),
module_manager_(&Module::ModuleManager::GetInstance()) {
ui_->setupUi(this);
diff --git a/src/ui/dialog/controller/ModuleControllerDialog.h b/src/ui/dialog/controller/ModuleControllerDialog.h
index de6cf4cd..94520a76 100644
--- a/src/ui/dialog/controller/ModuleControllerDialog.h
+++ b/src/ui/dialog/controller/ModuleControllerDialog.h
@@ -29,6 +29,7 @@
#pragma once
#include "core/module/Module.h"
+#include "ui/dialog/GeneralDialog.h"
class Ui_ModuleControllerDialog;
@@ -36,7 +37,7 @@ namespace GpgFrontend::UI {
class ModuleListView;
-class ModuleControllerDialog : public QDialog {
+class ModuleControllerDialog : public GeneralDialog {
Q_OBJECT
public:
/**
diff --git a/src/ui/dialog/controller/SmartCardControllerDialog.cpp b/src/ui/dialog/controller/SmartCardControllerDialog.cpp
index 847836b1..c698d9b0 100644
--- a/src/ui/dialog/controller/SmartCardControllerDialog.cpp
+++ b/src/ui/dialog/controller/SmartCardControllerDialog.cpp
@@ -28,8 +28,13 @@
#include "SmartCardControllerDialog.h"
+#include "core/function/gpg/GpgAdvancedOperator.h"
#include "core/function/gpg/GpgAssuanHelper.h"
+#include "core/function/gpg/GpgCommandExecutor.h"
+#include "core/function/gpg/GpgSmartCardManager.h"
+#include "core/module/ModuleManager.h"
#include "core/utils/GpgUtils.h"
+#include "ui/UISignalStation.h"
//
#include "ui_SmartCardControllerDialog.h"
@@ -48,54 +53,132 @@ SmartCardControllerDialog::SmartCardControllerDialog(QWidget* parent)
connect(ui_->keyDBIndexComboBox,
qOverload<int>(&QComboBox::currentIndexChanged), this,
- [=](int index) {
- channel_ = index;
- ui_->cardKeysTreeView->SetChannel(channel_);
- ui_->cardKeysTreeView->expandAll();
+ [=](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);
});
- slot_refresh();
+ 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) {
+ GpgFrontend::GpgAdvancedOperator::RestartGpgComponents(
+ [=](int err, DataObjectPtr) {
+ if (err >= 0) {
+ 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(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);
+ timer_->start(3000);
}
-void SmartCardControllerDialog::get_smart_card_serial_number() {
- auto [ret, status] = GpgAssuanHelper::GetInstance().SendStatusCommand(
- GpgComponentType::kGPG_AGENT, "SCD SERIALNO --all openpgp");
- if (!ret || status.isEmpty()) return;
+void SmartCardControllerDialog::select_smart_card_by_serial_number(
+ const QString& serial_number) {
+ if (serial_number.isEmpty()) {
+ reset_status();
+ return;
+ }
+
+ auto [ret, status] = GpgAssuanHelper::GetInstance(channel_).SendStatusCommand(
+ GpgComponentType::kGPG_AGENT,
+ QString("SCD SERIALNO --demand=%1 openpgp").arg(serial_number));
+ if (!ret || status.isEmpty()) {
+ reset_status();
+ return;
+ }
auto line = status.front();
auto token = line.split(' ');
if (token.size() != 2) {
LOG_E() << "invalid response of command SERIALNO: " << line;
+ reset_status();
return;
}
- serial_number_ = token.back().trimmed();
- LOG_D() << "get smart card serial number: " << serial_number_;
+ 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() {
- if (serial_number_.isEmpty()) return;
+void SmartCardControllerDialog::fetch_smart_card_info(
+ const QString& serial_number) {
+ if (!has_card_) return;
- auto [ret, status] = GpgAssuanHelper::GetInstance().SendStatusCommand(
- GpgComponentType::kGPG_AGENT, "SCD LEARN --force " + serial_number_);
- if (!ret || status.isEmpty()) return;
+ auto [ret, status] = GpgAssuanHelper::GetInstance(channel_).SendStatusCommand(
+ GpgComponentType::kGPG_AGENT, "SCD LEARN --force " + serial_number);
+ if (!ret || status.isEmpty()) {
+ reset_status();
+ return;
+ }
- LOG_D() << "fetch card raw info: " << status;
card_info_ = GpgOpenPGPCard(status);
-
if (!card_info_.good) {
- LOG_E() << "parse card raw info failed";
+ LOG_E() << "parse card raw status failed: " << status;
+ reset_status();
return;
}
+
+ 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 (!card_info_.good) return;
+ if (!has_card_) return;
QString html;
QTextStream out(&html);
- auto card = card_info_;
+ const auto& card = card_info_;
out << "<h2>" << tr("OpenPGP Card Information") << "</h2>";
@@ -109,6 +192,8 @@ void SmartCardControllerDialog::print_smart_card_info() {
out << "<li><b>" << tr("App Type:") << "</b> " << card.app_type << "</li>";
out << "<li><b>" << tr("App Version:") << "</b> " << card.app_version
<< "</li>";
+ out << "<li><b>" << tr("Manufacturer ID:") << "</b> " << card.manufacturer_id
+ << "</li>";
out << "<li><b>" << tr("Manufacturer:") << "</b> " << card.manufacturer
<< "</li>";
out << "<li><b>" << tr("Card Holder:") << "</b> " << card.card_holder
@@ -119,26 +204,104 @@ void SmartCardControllerDialog::print_smart_card_info() {
out << "</ul>";
out << "<h3>" << tr("Status") << "</h3><ul>";
- out << "<li><b>" << tr("CHV Status:") << "</b> " << card.chv_status
- << "</li>";
out << "<li><b>" << tr("Signature Counter:") << "</b> " << card.sig_counter
<< "</li>";
- out << "<li><b>" << tr("KDF:") << "</b> " << card.kdf << "</li>";
- out << "<li><b>" << tr("UIF1:") << "</b> " << card.uif1 << "</li>";
- out << "<li><b>" << tr("UIF2:") << "</b> " << card.uif2 << "</li>";
- out << "<li><b>" << tr("UIF3:") << "</b> " << card.uif3 << "</li>";
+ out << "<li><b>" << tr("CHV1 Cached:") << "</b> " << card.chv1_cached
+ << "</li>";
+ out << "<li><b>" << tr("CHV Max Length:") << "</b> "
+ << QString("%1, %2, %3")
+ .arg(card.chv_max_len[0])
+ .arg(card.chv_max_len[1])
+ .arg(card.chv_max_len[2])
+ << "</li>";
+ out << "<li><b>" << tr("CHV Retry Left:") << "</b> "
+ << QString("%1, %2, %3")
+ .arg(card.chv_retry[0])
+ .arg(card.chv_retry[1])
+ .arg(card.chv_retry[2])
+ << "</li>";
+ out << "<li><b>" << tr("KDF Status:") << "</b> ";
+ 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 << "</li>";
+ out << "<li><b>" << tr("UIF:") << "</b><ul>";
+ out << "<li>"
+ << tr("Sign: %1").arg(card.uif.sign ? tr("✔ Enabled") : tr("❌ Disabled"))
+ << "</li>";
+ out << "<li>"
+ << tr("Encrypt: %1")
+ .arg(card.uif.encrypt ? tr("✔ Enabled") : tr("❌ Disabled"))
+ << "</li>";
+ out << "<li>"
+ << tr("Authenticate: %1")
+ .arg(card.uif.auth ? tr("✔ Enabled") : tr("❌ Disabled"))
+ << "</li>";
+ out << "</ul></li>";
out << "</ul>";
- out << "<h3>" << tr("Fingerprints") << "</h3><ul>";
- for (auto it = card.fprs.begin(); it != card.fprs.end(); ++it) {
- out << "<li><b>" << tr("Key %1:").arg(it.key()) << "</b> " << it.value()
- << "</li>";
+ out << "<h3>" << tr("Key Information") << "</h3>";
+ out << "<br />";
+
+ if (card.card_keys_info.isEmpty()) {
+ out << "<i>" << tr("No key information available.") << "</i>";
+ } else {
+ out << "<table border='1' cellspacing='0' cellpadding='4'>";
+ out << "<tr><th>" << tr("No.") << "</th><th>" << tr("Fingerprint")
+ << "</th><th>" << tr("Created") << "</th><th>" << tr("Grip")
+ << "</th><th>" << tr("Type") << "</th><th>" << tr("Algorithm")
+ << "</th><th>" << tr("Usage") << "</th><th>" << tr("Curve")
+ << "</th></tr>";
+
+ for (auto it = card.card_keys_info.begin(); it != card.card_keys_info.end();
+ ++it) {
+ const auto& info = it.value();
+ out << "<tr><td>" << it.key() << "</td><td>" << info.fingerprint
+ << "</td><td>" << info.created.toString(Qt::ISODate) << "</td><td>"
+ << info.grip << "</td><td>" << info.key_type << "</td><td>"
+ << info.algo << "</td><td>" << info.usage << "</td><td>" << info.algo
+ << "</td></tr>";
+ }
+
+ out << "</table>";
}
+
+ out << "<br />";
+
+ out << "<h3>" << tr("Extended Capabilities") << "</h3><ul>";
+ out << "<li>"
+ << tr("Key Info (ki): %1").arg(card.ext_cap.ki ? tr("Yes") : tr("No"))
+ << "</li>";
+ out << "<li>"
+ << tr("Additional Auth (aac): %1")
+ .arg(card.ext_cap.aac ? tr("Yes") : tr("No"))
+ << "</li>";
+ out << "<li>"
+ << tr("Biometric Terminal (bt): %1")
+ .arg(card.ext_cap.bt ? tr("Yes") : tr("No"))
+ << "</li>";
+ out << "<li>"
+ << tr("KDF Supported: %1").arg(card.ext_cap.kdf ? tr("Yes") : tr("No"))
+ << "</li>";
+ out << "<li>" << tr("Status Indicator: %1").arg(card.ext_cap.status_indicator)
+ << "</li>";
out << "</ul>";
- if (!card.card_infos.isEmpty()) {
+ if (!card.additional_card_infos.isEmpty()) {
out << "<h3>" << tr("Additional Info") << "</h3><ul>";
- for (auto it = card.card_infos.begin(); it != card.card_infos.end(); ++it) {
+ for (auto it = card.additional_card_infos.begin();
+ it != card.additional_card_infos.end(); ++it) {
out << "<li><b>" << tr("%1:").arg(it.key()) << "</b> " << it.value()
<< "</li>";
}
@@ -149,25 +312,343 @@ void SmartCardControllerDialog::print_smart_card_info() {
}
void SmartCardControllerDialog::slot_refresh() {
- ui_->cardInfoEdit->clear();
+ fetch_smart_card_info(ui_->currentCardComboBox->currentText());
+}
- get_smart_card_serial_number();
- fetch_smart_card_info();
+void SmartCardControllerDialog::refresh_key_tree_view(int channel) {
+ if (!has_card_) return;
- print_smart_card_info();
- refresh_key_tree_view();
-}
+ QStringList card_fprs;
+ for (const auto& key_info : card_info_.card_keys_info.values()) {
+ card_fprs.append(key_info.fingerprint);
+ }
-void SmartCardControllerDialog::refresh_key_tree_view() {
- if (card_info_.fprs.isEmpty()) {
+ if (card_fprs.isEmpty()) {
ui_->cardKeysTreeView->SetKeyFilter([](auto) { return false; });
return;
}
- QStringList card_fprs(card_info_.fprs.begin(), card_info_.fprs.end());
+ ui_->cardKeysTreeView->SetChannel(channel);
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 << "<h2>" << tr("No OpenPGP Smart Card Found") << "</h2>";
+ out << "<p>" << tr("No OpenPGP-compatible smart card has been detected.")
+ << "</p>";
+
+ out << "<p>"
+ << 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.")
+ << "</p>";
+
+ out << "<p>"
+ << tr("Make sure your card is inserted and properly recognized by "
+ "the system. You can also try reconnecting the card or "
+ "restarting the application.")
+ << "</p>";
+
+ out << "<p>" << tr("Read the GnuPG Smart Card HOWTO: ")
+ << "https://gnupg.org/howtos/card-howto/en/" << "</p>";
+
+ ui_->cardInfoEdit->setText(html);
+}
+
+void SmartCardControllerDialog::slot_listen_smart_card_changes() {
+ auto [r, s] = GpgAssuanHelper::GetInstance(channel_).SendStatusCommand(
+ GpgComponentType::kGPG_AGENT, "SCD SERIALNO --all");
+ if (!r) {
+ LOG_D() << "command SCD SERIALNO --all failed, resetting...";
+ ui_->currentCardComboBox->clear();
+ cached_status_hash_.clear();
+ reset_status();
+ return;
+ }
+
+ auto current_status_hash =
+ QCryptographicHash::hash(s.join(' ').toUtf8(), QCryptographicHash::Sha1)
+ .toHex();
+ // check and skip
+ if (cached_status_hash_ == current_status_hash) return;
+
+ cached_status_hash_.clear();
+ ui_->currentCardComboBox->clear();
+
+ auto [ret, status] = GpgAssuanHelper::GetInstance(channel_).SendStatusCommand(
+ GpgComponentType::kGPG_AGENT, "SCD GETINFO all_active_apps");
+ if (!r) {
+ LOG_D() << "command SCD SERIALNO --all failed, resetting...";
+ return;
+ }
+
+ int index = 0;
+ for (const auto& line : status) {
+ auto tokens = line.split(' ');
+
+ if (tokens.size() < 2 || tokens[0] != "SERIALNO") {
+ LOG_E() << "invalid response of command GETINFO all_active_apps: "
+ << line;
+ continue;
+ }
+
+ auto serial_number = tokens[1];
+
+ if (!line.contains("openpgp")) {
+ LOG_W() << "smart card: " << serial_number << "doesn't support openpgp.";
+ continue;
+ }
+
+ ui_->currentCardComboBox->insertItem(index++, serial_number);
+ }
+
+ if (ui_->currentCardComboBox->currentText().isEmpty()) {
+ LOG_D() << "no inserted and supported smart card found.";
+ reset_status();
+ return;
+ }
+
+ cached_status_hash_ = current_status_hash;
+ ui_->currentCardComboBox->setCurrentIndex(0);
+ select_smart_card_by_serial_number(ui_->currentCardComboBox->currentText());
+}
+
+void SmartCardControllerDialog::slot_disable_controllers(bool disable) {
+ ui_->groupBox->setDisabled(disable);
+ ui_->keyDBIndexComboBox->setDisabled(disable);
+ ui_->cardKeysTreeView->setDisabled(disable);
+}
+
+void SmartCardControllerDialog::slot_fetch_smart_card_keys() {
+ GpgSmartCardManager::GetInstance().Fetch(
+ ui_->currentCardComboBox->currentText());
+
+ QTimer::singleShot(1000, [=]() {
+ GpgCommandExecutor::GetInstance(channel_).GpgExecuteSync(
+ {{},
+ {"--card-status"},
+ [=](int exit_code, const QString&, const QString&) {
+ LOG_D() << "gpg --card--status exit code: " << exit_code;
+ if (exit_code != 0) return;
+
+ emit UISignalStation::GetInstance() -> SignalKeyDatabaseRefresh();
+ }});
+ });
+}
+
+auto PercentDataEscape(const QByteArray& data, bool plus_escape = false,
+ const QString& prefix = QString()) -> QString {
+ QString result;
+
+ if (!prefix.isEmpty()) {
+ for (QChar ch : prefix) {
+ if (ch == '%' || ch.unicode() < 0x20) {
+ result += QString("%%%1")
+ .arg(ch.unicode(), 2, 16, QLatin1Char('0'))
+ .toUpper();
+ } else {
+ result += ch;
+ }
+ }
+ }
+
+ for (unsigned char ch : data) {
+ if (ch == '\0') {
+ result += "%00";
+ } else if (ch == '%') {
+ result += "%25";
+ } else if (plus_escape && ch == ' ') {
+ result += '+';
+ } else if (plus_escape && (ch < 0x20 || ch == '+')) {
+ result += QString("%%%1").arg(ch, 2, 16, QLatin1Char('0')).toUpper();
+ } else {
+ result += QLatin1Char(ch);
+ }
+ }
+
+ return result;
+}
+
+auto AskIsoDisplayName(QWidget* parent, bool* ok) -> QString {
+ QString surname = QInputDialog::getText(
+ parent, QObject::tr("Cardholder's Surname"),
+ QObject::tr("Please enter your surname (e.g., Lee):"), QLineEdit::Normal,
+ "", ok);
+ if (!*ok || surname.trimmed().isEmpty()) return QString();
+
+ QString given_name = QInputDialog::getText(
+ parent, QObject::tr("Cardholder's Given Name"),
+ QObject::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, QObject::tr("Too Long"),
+ QObject::tr("Combined name too long (max 39 characters)."));
+ *ok = false;
+ return QString();
+ }
+
+ return iso_name;
+}
+
+void SmartCardControllerDialog::modify_key_attribute(const QString& attr) {
+ QString value;
+ bool ok = false;
+
+ if (attr == "DISP-SEX") {
+ QStringList options;
+ options << tr("1 - Male") << tr("2 - 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;
+ }
+ }
+
+ const auto command = QString("SCD SETATTR %1 ").arg(attr);
+ const auto escaped_command =
+ PercentDataEscape(value.trimmed().toUtf8(), true, command);
+
+ auto [r, s] = GpgAssuanHelper::GetInstance(channel_).SendStatusCommand(
+ GpgComponentType::kGPG_AGENT, escaped_command);
+
+ if (!r) {
+ LOG_D() << "SCD SETATTR command failed for attr" << attr;
+ QMessageBox::critical(this, tr("Failed"),
+ tr("Failed to set attribute '%1'. The card may "
+ "reject it or require a PIN.")
+ .arg(attr));
+ return;
+ }
+
+ fetch_smart_card_info(ui_->currentCardComboBox->currentText());
+}
+
+void SmartCardControllerDialog::modify_key_pin(const QString& pinref) {
+ if (pinref.isEmpty()) {
+ QMessageBox::warning(this, tr("Error"), tr("PIN reference is empty."));
+ return;
+ }
+
+ QString command;
+ if (pinref == "OPENPGP.1") {
+ command = "SCD PASSWD OPENPGP.1";
+ } else if (pinref == "OPENPGP.3") {
+ command = "SCD PASSWD OPENPGP.3";
+ } else if (pinref == "OPENPGP.2") {
+ command = "SCD PASSWD --reset OPENPGP.2";
+ } else {
+ command = QString("SCD PASSWD %1").arg(pinref);
+ }
+
+ auto [success, status] =
+ GpgAssuanHelper::GetInstance(channel_).SendStatusCommand(
+ GpgComponentType::kGPG_AGENT, command);
+
+ if (!success) {
+ QString message;
+ if (pinref == "OPENPGP.3") {
+ message = tr("Failed to change Admin PIN.");
+ } else if (pinref == "OPENPGP.2") {
+ message = tr("Failed to set the Reset Code.");
+ } else {
+ message = tr("Failed to change PIN.");
+ }
+
+ QMessageBox::critical(this, tr("Error"), message);
+ LOG_E() << "assuan command failed: " << command;
+ return;
+ }
+
+ QMessageBox::information(this, tr("Success"),
+ tr("PIN operation completed successfully."));
+ fetch_smart_card_info(ui_->currentCardComboBox->currentText());
+}
+
+// bool SmartCardControllerDialog::generate_card_key(const QString& keyref,
+// bool force,
+// const QString& algo,
+// QDateTime timestamp) {
+// if (keyref.isEmpty()) {
+// QMessageBox::warning(this, tr("Error"),
+// tr("Key reference cannot be empty."));
+// return false;
+// }
+
+// QStringList cmd_parts;
+// cmd_parts << "SCD GENKEY";
+
+// if (timestamp.isValid()) {
+// QString iso_time = timestamp.toString("yyyyMMddTHHmmss");
+// cmd_parts << QString("--timestamp=%1").arg(iso_time);
+// }
+
+// if (force) {
+// cmd_parts << "--force";
+// }
+
+// if (!algo.isEmpty()) {
+// cmd_parts << QString("--algo=%1").arg(algo);
+// }
+
+// cmd_parts << keyref;
+
+// const QString command = cmd_parts.join(' ');
+// LOG_D() << "sending assuan command: " << command;
+
+// auto [ok, status] =
+// GpgAssuanHelper::GetInstance(channel_).SendStatusCommand(
+// GpgComponentType::kGPG_AGENT, command);
+
+// if (!ok) {
+// QMessageBox::critical(this, tr("Generation Failed"),
+// tr("Failed to generate key for
+// '%1'.").arg(keyref));
+// return false;
+// }
+
+// QMessageBox::information(
+// this, tr("Success"),
+// tr("Key generation for '%1' completed successfully.").arg(keyref));
+// fetch_smart_card_info(ui_->currentCardComboBox->currentText());
+// return true;
+// }
+
} // namespace GpgFrontend::UI
diff --git a/src/ui/dialog/controller/SmartCardControllerDialog.h b/src/ui/dialog/controller/SmartCardControllerDialog.h
index 9b71bba7..83a717ee 100644
--- a/src/ui/dialog/controller/SmartCardControllerDialog.h
+++ b/src/ui/dialog/controller/SmartCardControllerDialog.h
@@ -29,6 +29,7 @@
#pragma once
#include "core/model/GpgOpenPGPCard.h"
+#include "core/typedef/CoreTypedef.h"
#include "ui/dialog/GeneralDialog.h"
class Ui_SmartCardControllerDialog;
@@ -52,23 +53,44 @@ class SmartCardControllerDialog : public GeneralDialog {
*/
void slot_refresh();
+ /**
+ * @brief
+ *
+ */
+ void slot_listen_smart_card_changes();
+
+ /**
+ * @brief
+ *
+ * @param disable
+ */
+ void slot_disable_controllers(bool disable);
+
+ /**
+ * @brief
+ *
+ */
+ void slot_fetch_smart_card_keys();
+
private:
QSharedPointer<Ui_SmartCardControllerDialog> ui_; ///<
int channel_;
- QString serial_number_;
+ bool has_card_;
GpgOpenPGPCard card_info_;
+ QString cached_status_hash_;
+ QTimer* timer_;
/**
* @brief Get the smart card serial number object
*
*/
- void get_smart_card_serial_number();
+ void select_smart_card_by_serial_number(const QString& serial_number);
/**
* @brief
*
*/
- void fetch_smart_card_info();
+ void fetch_smart_card_info(const QString& serial_number);
/**
* @brief
@@ -80,6 +102,26 @@ class SmartCardControllerDialog : public GeneralDialog {
* @brief
*
*/
- void refresh_key_tree_view();
+ void refresh_key_tree_view(int channel);
+
+ /**
+ * @brief
+ *
+ */
+ void reset_status();
+
+ /**
+ * @brief
+ *
+ * @param attr
+ */
+ void modify_key_attribute(const QString& attr);
+
+ /**
+ * @brief
+ *
+ * @param attr
+ */
+ void modify_key_pin(const QString& pinref);
};
} // namespace GpgFrontend::UI
diff --git a/src/ui/main_window/GeneralMainWindow.cpp b/src/ui/main_window/GeneralMainWindow.cpp
index 76eda3fc..ebd4049f 100644
--- a/src/ui/main_window/GeneralMainWindow.cpp
+++ b/src/ui/main_window/GeneralMainWindow.cpp
@@ -42,6 +42,9 @@ GpgFrontend::UI::GeneralMainWindow::GeneralMainWindow(QString id,
: QMainWindow(parent), id_(std::move(id)) {
UIModuleManager::GetInstance().RegisterQObject(id_, this);
slot_restore_settings();
+
+ // should delete itself at closing by default
+ setAttribute(Qt::WA_DeleteOnClose);
}
GpgFrontend::UI::GeneralMainWindow::~GeneralMainWindow() = default;
diff --git a/src/ui/widgets/KeyTreeView.cpp b/src/ui/widgets/KeyTreeView.cpp
index 3df5a440..c0a00c4e 100644
--- a/src/ui/widgets/KeyTreeView.cpp
+++ b/src/ui/widgets/KeyTreeView.cpp
@@ -32,6 +32,7 @@
#include "core/function/gpg/GpgKeyGetter.h"
#include "core/utils/GpgUtils.h"
+#include "ui/UISignalStation.h"
#include "ui/dialog/keypair_details/KeyDetailsDialog.h"
#include "ui/model/GpgKeyTreeProxyModel.h"
@@ -121,6 +122,15 @@ void KeyTreeView::init() {
new KeyDetailsDialog(model_->GetGpgContextChannel(), key, this);
});
+
+ connect(UISignalStation::GetInstance(),
+ &UISignalStation::SignalKeyDatabaseRefresh, this, [=] {
+ model_ = QSharedPointer<GpgKeyTreeModel>::create(
+ channel_, GpgKeyGetter::GetInstance(channel_).FetchKey(),
+ [](auto) { return false; }, this);
+ proxy_model_.setSourceModel(model_.get());
+ proxy_model_.invalidate();
+ });
}
void KeyTreeView::SetKeyFilter(const GpgKeyTreeProxyModel::KeyFilter& filter) {