aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsaturneric <[email protected]>2025-04-12 23:03:57 +0000
committersaturneric <[email protected]>2025-04-12 23:03:57 +0000
commit571cfb16ccfd7ac6bc59b5acc77a94d0bdcf0990 (patch)
treea4efe7ba4d5344897c60513c03172d5de0191658
parentfix: upgrade to gpgme 1.24.2 (diff)
downloadGpgFrontend-571cfb16ccfd7ac6bc59b5acc77a94d0bdcf0990.tar.gz
GpgFrontend-571cfb16ccfd7ac6bc59b5acc77a94d0bdcf0990.zip
feat: add openpgp smart card support
-rw-r--r--gpgfrontend.qrc1
-rw-r--r--resource/lfs/icons/smart-card.pngbin0 -> 4242 bytes
-rw-r--r--src/core/model/GpgCardKeyPairInfo.cpp73
-rw-r--r--src/core/model/GpgCardKeyPairInfo.h48
-rw-r--r--src/core/model/GpgKey.cpp4
-rw-r--r--src/core/model/GpgKey.h9
-rw-r--r--src/core/model/GpgKeyTreeModel.cpp9
-rw-r--r--src/core/model/GpgKeyTreeModel.h7
-rw-r--r--src/core/model/GpgOpenPGPCard.cpp91
-rw-r--r--src/core/model/GpgOpenPGPCard.h71
-rw-r--r--src/core/model/GpgSubKey.cpp11
-rw-r--r--src/core/model/GpgSubKey.h11
-rw-r--r--src/core/utils/CommonUtils.cpp9
-rw-r--r--src/core/utils/CommonUtils.h9
-rw-r--r--src/core/utils/GpgUtils.cpp17
-rw-r--r--src/core/utils/GpgUtils.h10
-rw-r--r--src/test/core/GpgCoreTestAssuan.cpp1
-rw-r--r--src/ui/dialog/controller/SmartCardControllerDialog.cpp173
-rw-r--r--src/ui/dialog/controller/SmartCardControllerDialog.h85
-rw-r--r--src/ui/dialog/keypair_details/KeyPairSubkeyTab.cpp7
-rw-r--r--src/ui/main_window/MainWindow.h6
-rw-r--r--src/ui/main_window/MainWindowUI.cpp10
-rw-r--r--src/ui/model/GpgKeyTreeProxyModel.cpp5
-rw-r--r--src/ui/model/GpgKeyTreeProxyModel.h25
-rw-r--r--src/ui/widgets/KeyTreeView.cpp87
-rw-r--r--src/ui/widgets/KeyTreeView.h26
-rw-r--r--ui/SmartCardControllerDialog.ui120
27 files changed, 897 insertions, 28 deletions
diff --git a/gpgfrontend.qrc b/gpgfrontend.qrc
index 49a26dec..4f1e2275 100644
--- a/gpgfrontend.qrc
+++ b/gpgfrontend.qrc
@@ -111,6 +111,7 @@
<file alias="batch.png">resource/lfs/icons/batch.png</file>
<file alias="encr-sign.png">resource/lfs/icons/encr-sign.png</file>
<file alias="decr-verify.png">resource/lfs/icons/decr-verify.png</file>
+ <file alias="smart-card.png">resource/lfs/icons/smart-card.png</file>
</qresource>
<qresource prefix="/test/key">
<file alias="pv1.key">resource/lfs/test/data/pv1.key</file>
diff --git a/resource/lfs/icons/smart-card.png b/resource/lfs/icons/smart-card.png
new file mode 100644
index 00000000..10d23d01
--- /dev/null
+++ b/resource/lfs/icons/smart-card.png
Binary files differ
diff --git a/src/core/model/GpgCardKeyPairInfo.cpp b/src/core/model/GpgCardKeyPairInfo.cpp
new file mode 100644
index 00000000..481d88ea
--- /dev/null
+++ b/src/core/model/GpgCardKeyPairInfo.cpp
@@ -0,0 +1,73 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "GpgCardKeyPairInfo.h"
+
+namespace GpgFrontend {
+
+GpgCardKeyPairInfo::GpgCardKeyPairInfo(const QString &status) {
+ const auto values = status.split(QLatin1Char(' '));
+ if (values.size() < 2) {
+ return;
+ }
+
+ grip = values[0];
+ key_ref = values[1];
+ if (values.size() >= 3) {
+ usage = values[2];
+ }
+
+ if (values.size() >= 4 && !values[3].isEmpty() && values[3] != "-") {
+ bool ok;
+ const qint64 seconds_since_epoch = values[3].toLongLong(&ok);
+ if (ok) {
+ time =
+ QDateTime::fromSecsSinceEpoch(seconds_since_epoch, QTimeZone::utc());
+ }
+ }
+
+ if (values.size() >= 5) {
+ algorithm = values[4];
+ }
+}
+
+auto GpgCardKeyPairInfo::CanAuthenticate() const -> bool {
+ return usage.contains('a');
+}
+
+auto GpgCardKeyPairInfo::CanCertify() const -> bool {
+ return usage.contains('c');
+}
+
+auto GpgCardKeyPairInfo::CanEncrypt() const -> bool {
+ return usage.contains('e');
+}
+
+auto GpgCardKeyPairInfo::CanSign() const -> bool { return usage.contains('s'); }
+
+} // namespace GpgFrontend
diff --git a/src/core/model/GpgCardKeyPairInfo.h b/src/core/model/GpgCardKeyPairInfo.h
new file mode 100644
index 00000000..132ded84
--- /dev/null
+++ b/src/core/model/GpgCardKeyPairInfo.h
@@ -0,0 +1,48 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+namespace GpgFrontend {
+
+struct GpgCardKeyPairInfo {
+ explicit GpgCardKeyPairInfo(const QString &status);
+
+ [[nodiscard]] auto CanAuthenticate() const -> bool;
+ [[nodiscard]] auto CanCertify() const -> bool;
+ [[nodiscard]] auto CanEncrypt() const -> bool;
+ [[nodiscard]] auto CanSign() const -> bool;
+
+ QString key_ref;
+ QString grip;
+ QString usage;
+ QDateTime time;
+ QString algorithm;
+};
+
+} // namespace GpgFrontend
diff --git a/src/core/model/GpgKey.cpp b/src/core/model/GpgKey.cpp
index 6608d885..3efb53fc 100644
--- a/src/core/model/GpgKey.cpp
+++ b/src/core/model/GpgKey.cpp
@@ -37,6 +37,9 @@ GpgKey::GpgKey(gpgme_key_t key)
if (ptr != nullptr) gpgme_key_unref(ptr);
}) {}
+GpgKey::GpgKey(QSharedPointer<struct _gpgme_key> key_ref)
+ : key_ref_(std::move(key_ref)) {}
+
GpgKey::operator gpgme_key_t() const { return key_ref_.get(); }
GpgKey::GpgKey(const GpgKey &) = default;
@@ -218,4 +221,5 @@ auto GpgKey::PrimaryKey() const -> GpgSubKey {
}
auto GpgKey::IsSubKey() const -> bool { return false; }
+
} // namespace GpgFrontend \ No newline at end of file
diff --git a/src/core/model/GpgKey.h b/src/core/model/GpgKey.h
index da92de8d..de2e8370 100644
--- a/src/core/model/GpgKey.h
+++ b/src/core/model/GpgKey.h
@@ -28,6 +28,8 @@
#pragma once
+#include <utility>
+
#include "core/model/GpgAbstractKey.h"
#include "core/model/GpgSubKey.h"
#include "core/model/GpgUID.h"
@@ -57,6 +59,13 @@ class GPGFRONTEND_CORE_EXPORT GpgKey : public GpgAbstractKey {
/**
* @brief Construct a new Gpg Key object
*
+ * @param key
+ */
+ explicit GpgKey(QSharedPointer<struct _gpgme_key> key_ref);
+
+ /**
+ * @brief Construct a new Gpg Key object
+ *
* @param k
*/
GpgKey(const GpgKey&);
diff --git a/src/core/model/GpgKeyTreeModel.cpp b/src/core/model/GpgKeyTreeModel.cpp
index cb3ee4ba..bbe5d00b 100644
--- a/src/core/model/GpgKeyTreeModel.cpp
+++ b/src/core/model/GpgKeyTreeModel.cpp
@@ -257,6 +257,15 @@ auto GpgKeyTreeModel::GetAllCheckedSubKey() -> QContainer<GpgSubKey> {
return ret;
}
+auto GpgKeyTreeModel::GetKeyByIndex(QModelIndex index) -> GpgAbstractKey * {
+ if (!index.isValid()) return nullptr;
+
+ const auto *item =
+ static_cast<const GpgKeyTreeItem *>(index.internalPointer());
+
+ return item->Key();
+}
+
GpgKeyTreeItem::GpgKeyTreeItem(QSharedPointer<GpgAbstractKey> key,
QVariantList data)
: data_(std::move(data)), key_(std::move(key)) {}
diff --git a/src/core/model/GpgKeyTreeModel.h b/src/core/model/GpgKeyTreeModel.h
index f56591ea..9e91037d 100644
--- a/src/core/model/GpgKeyTreeModel.h
+++ b/src/core/model/GpgKeyTreeModel.h
@@ -346,6 +346,13 @@ class GPGFRONTEND_CORE_EXPORT GpgKeyTreeModel : public QAbstractItemModel {
*/
auto GetAllCheckedSubKey() -> QContainer<GpgSubKey>;
+ /**
+ * @brief Get the Key By Index object
+ *
+ * @return GpgAbstractKey*
+ */
+ auto GetKeyByIndex(QModelIndex) -> GpgAbstractKey *;
+
private:
int gpg_context_channel_;
QVariantList column_headers_;
diff --git a/src/core/model/GpgOpenPGPCard.cpp b/src/core/model/GpgOpenPGPCard.cpp
new file mode 100644
index 00000000..f0d8c9cd
--- /dev/null
+++ b/src/core/model/GpgOpenPGPCard.cpp
@@ -0,0 +1,91 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "GpgOpenPGPCard.h"
+
+#include "core/model/GpgCardKeyPairInfo.h"
+#include "core/utils/CommonUtils.h"
+
+namespace GpgFrontend {
+
+void GpgFrontend::GpgOpenPGPCard::parse_card_info(const QString& name,
+ const QString& value) {
+ if (name == "APPVERSION") {
+ app_version = ParseHexEncodedVersionTuple(value);
+ } else if (name == "CARDTYPE") {
+ card_type = value;
+ } else if (name == "CARDVERSION") {
+ card_version = ParseHexEncodedVersionTuple(value);
+ } else if (name == "DISP-NAME") {
+ auto list = value.split(QStringLiteral("<<"), Qt::SkipEmptyParts);
+ std::reverse(list.begin(), list.end());
+ card_holder =
+ list.join(QLatin1Char(' ')).replace(QLatin1Char('<'), QLatin1Char(' '));
+ } else if (name == "KEYPAIRINFO") {
+ const GpgCardKeyPairInfo info = GpgCardKeyPairInfo(value);
+ if (info.grip.isEmpty()) {
+ LOG_W() << "invalid KEYPAIRINFO status line" << value;
+ good = false;
+ }
+ } else if (name == "KEY-FPR") {
+ const auto values = value.split(QLatin1Char(' '));
+ if (values.size() < 2) {
+ LOG_W() << "invalid KEY-FPR status line" << value;
+ good = false;
+ return;
+ }
+
+ const auto& key_number = values[0].toInt();
+ const auto& fpr = values[1];
+ fprs.insert(key_number, fpr);
+
+ } else if (name == "MANUFACTURER") {
+ // the value of 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
+ // for PKCS#15 cards the manufacturer ID is always 0, e.g.
+ // 0 www.atos.net/cardos [R&S]
+ auto space_index = value.indexOf(' ');
+ if (space_index != -1) {
+ card_infos.insert(name, value.mid(space_index + 1).trimmed());
+ }
+ } else {
+ card_infos.insert(name, value);
+ }
+}
+GpgOpenPGPCard::GpgOpenPGPCard(const QStringList& status) : good(true) {
+ for (const QString& line : status) {
+ auto tokens = line.split(' ', Qt::SkipEmptyParts);
+ auto name = tokens.value(0);
+ auto value = tokens.mid(1).join(' ');
+
+ parse_card_info(name, value);
+ }
+}
+} // namespace GpgFrontend
diff --git a/src/core/model/GpgOpenPGPCard.h b/src/core/model/GpgOpenPGPCard.h
new file mode 100644
index 00000000..78b7d854
--- /dev/null
+++ b/src/core/model/GpgOpenPGPCard.h
@@ -0,0 +1,71 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include "core/model/GpgCardKeyPairInfo.h"
+#include "core/typedef/CoreTypedef.h"
+
+namespace GpgFrontend {
+
+struct GPGFRONTEND_CORE_EXPORT GpgOpenPGPCard {
+ public:
+ QString reader;
+ QString serial_number;
+ QString card_type;
+ int card_version;
+ QString app_type;
+ int app_version;
+ QString ext_capability;
+ QString manufacturer;
+ QString card_holder;
+ QString display_language;
+ QString display_sex;
+ QString chv_status;
+ int sig_counter = 0;
+
+ QContainer<GpgCardKeyPairInfo> keys;
+ QMap<int, QString> fprs;
+ QMap<QString, QString> card_infos;
+
+ QString kdf;
+ QString uif1;
+ QString uif2;
+ QString uif3;
+
+ bool good = false;
+
+ GpgOpenPGPCard() = default;
+
+ explicit GpgOpenPGPCard(const QStringList& status);
+
+ private:
+ void parse_card_info(const QString& name, const QString& value);
+};
+
+} // namespace GpgFrontend \ No newline at end of file
diff --git a/src/core/model/GpgSubKey.cpp b/src/core/model/GpgSubKey.cpp
index b4bd94ec..a4e6582f 100644
--- a/src/core/model/GpgSubKey.cpp
+++ b/src/core/model/GpgSubKey.cpp
@@ -27,8 +27,7 @@
*/
#include "GpgSubKey.h"
-#include <utility>
-
+#include "core/model/GpgKey.h"
namespace GpgFrontend {
GpgSubKey::GpgSubKey() = default;
@@ -92,11 +91,15 @@ auto GpgSubKey::ExpirationTime() const -> QDateTime {
auto GpgSubKey::IsADSK() const -> bool { return s_key_ref_->can_renc; }
-auto GpgSubKey::SmartCardSerialNumber() -> QString {
- return s_key_ref_->card_number;
+auto GpgSubKey::SmartCardSerialNumber() const -> QString {
+ return QString::fromLatin1(s_key_ref_->card_number);
}
auto GpgSubKey::IsSubKey() const -> bool { return true; }
auto GpgSubKey::IsGood() const -> bool { return s_key_ref_ != nullptr; }
+
+auto GpgSubKey::Convert2GpgKey() const -> QSharedPointer<GpgKey> {
+ return QSharedPointer<GpgKey>::create(key_ref_);
+}
} // namespace GpgFrontend
diff --git a/src/core/model/GpgSubKey.h b/src/core/model/GpgSubKey.h
index fe925f8e..c947b8af 100644
--- a/src/core/model/GpgSubKey.h
+++ b/src/core/model/GpgSubKey.h
@@ -35,6 +35,8 @@
namespace GpgFrontend {
+class GpgKey;
+
/**
* @brief
*
@@ -232,7 +234,14 @@ class GPGFRONTEND_CORE_EXPORT GpgSubKey : public GpgAbstractKey {
*
* @return QString
*/
- [[nodiscard]] auto SmartCardSerialNumber() -> QString;
+ [[nodiscard]] auto SmartCardSerialNumber() const -> QString;
+
+ /**
+ * @brief
+ *
+ * @return QString
+ */
+ [[nodiscard]] auto Convert2GpgKey() const -> QSharedPointer<GpgKey>;
private:
QSharedPointer<struct _gpgme_key> key_ref_;
diff --git a/src/core/utils/CommonUtils.cpp b/src/core/utils/CommonUtils.cpp
index 9687acd4..0adc4d7f 100644
--- a/src/core/utils/CommonUtils.cpp
+++ b/src/core/utils/CommonUtils.cpp
@@ -98,4 +98,13 @@ auto GFUnStrDup(const char* s) -> QString {
auto GPGFRONTEND_CORE_EXPORT IsFlatpakENV() -> bool {
return QString::fromLocal8Bit(qgetenv("container")) == "flatpak";
}
+
+auto GPGFRONTEND_CORE_EXPORT ParseHexEncodedVersionTuple(const QString& s)
+ -> int {
+ // s is a hex-encoded, unsigned int-packed version tuple,
+ // i.e. each byte represents one part of the version tuple
+ bool ok;
+ const auto version = s.toUtf8().toUInt(&ok, 16);
+ return ok ? static_cast<int>(version) : -1;
+}
} // namespace GpgFrontend \ No newline at end of file
diff --git a/src/core/utils/CommonUtils.h b/src/core/utils/CommonUtils.h
index de77114f..468d8a59 100644
--- a/src/core/utils/CommonUtils.h
+++ b/src/core/utils/CommonUtils.h
@@ -73,4 +73,13 @@ auto GPGFRONTEND_CORE_EXPORT GFUnStrDup(const char *) -> QString;
*/
auto GPGFRONTEND_CORE_EXPORT IsFlatpakENV() -> bool;
+/**
+ * @brief
+ *
+ * @param s
+ * @return int
+ */
+auto GPGFRONTEND_CORE_EXPORT ParseHexEncodedVersionTuple(const QString &s)
+ -> int;
+
} // namespace GpgFrontend \ No newline at end of file
diff --git a/src/core/utils/GpgUtils.cpp b/src/core/utils/GpgUtils.cpp
index c7040cc2..22ba856e 100644
--- a/src/core/utils/GpgUtils.cpp
+++ b/src/core/utils/GpgUtils.cpp
@@ -353,4 +353,21 @@ auto GPGFRONTEND_CORE_EXPORT GetUsagesBySubkey(const GpgSubKey& key)
if (key.IsADSK()) usages += "R";
return usages;
}
+
+auto GPGFRONTEND_CORE_EXPORT GetGpgKeyByGpgAbstractKey(GpgAbstractKey* ab_key)
+ -> GpgKey {
+ if (!ab_key->IsGood()) return {};
+
+ if (ab_key->IsSubKey()) {
+ auto* s_key = dynamic_cast<GpgSubKey*>(ab_key);
+
+ assert(s_key != nullptr);
+ if (s_key == nullptr) return {};
+
+ return *s_key->Convert2GpgKey();
+ }
+
+ auto* key = dynamic_cast<GpgKey*>(ab_key);
+ return *key;
+}
} // namespace GpgFrontend
diff --git a/src/core/utils/GpgUtils.h b/src/core/utils/GpgUtils.h
index b453cc0a..9fcfe5cf 100644
--- a/src/core/utils/GpgUtils.h
+++ b/src/core/utils/GpgUtils.h
@@ -28,7 +28,7 @@
#pragma once
-#include "core/function/result_analyse/GpgResultAnalyse.h"
+#include "core/model/GpgAbstractKey.h"
#include "core/model/KeyDatabaseInfo.h"
#include "core/struct/settings_object/KeyDatabaseItemSO.h"
#include "core/typedef/CoreTypedef.h"
@@ -173,4 +173,12 @@ auto GPGFRONTEND_CORE_EXPORT GetUsagesByKey(const GpgKey& key) -> QString;
*/
auto GPGFRONTEND_CORE_EXPORT GetUsagesBySubkey(const GpgSubKey& key) -> QString;
+/**
+ * @brief
+ *
+ * @return GpgKey
+ */
+auto GPGFRONTEND_CORE_EXPORT GetGpgKeyByGpgAbstractKey(GpgAbstractKey*)
+ -> GpgKey;
+
} // namespace GpgFrontend \ No newline at end of file
diff --git a/src/test/core/GpgCoreTestAssuan.cpp b/src/test/core/GpgCoreTestAssuan.cpp
index cc61b27c..ef0d444f 100644
--- a/src/test/core/GpgCoreTestAssuan.cpp
+++ b/src/test/core/GpgCoreTestAssuan.cpp
@@ -70,6 +70,7 @@ TEST_F(GpgCoreTest, CoreAssuanConnectTestB) {
helper.SendStatusCommand(GpgComponentType::kGPG_AGENT, "keyinfo --list");
ASSERT_TRUE(ret);
ASSERT_TRUE(!status.isEmpty());
+ ASSERT_TRUE(status.front().startsWith("KEYINFO"));
LOG_D() << "status lines of command keyinfo --list: " << status;
}
diff --git a/src/ui/dialog/controller/SmartCardControllerDialog.cpp b/src/ui/dialog/controller/SmartCardControllerDialog.cpp
new file mode 100644
index 00000000..847836b1
--- /dev/null
+++ b/src/ui/dialog/controller/SmartCardControllerDialog.cpp
@@ -0,0 +1,173 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "SmartCardControllerDialog.h"
+
+#include "core/function/gpg/GpgAssuanHelper.h"
+#include "core/utils/GpgUtils.h"
+
+//
+#include "ui_SmartCardControllerDialog.h"
+
+namespace GpgFrontend::UI {
+SmartCardControllerDialog::SmartCardControllerDialog(QWidget* parent)
+ : GeneralDialog("SmartCardControllerDialog", parent),
+ ui_(QSharedPointer<Ui_SmartCardControllerDialog>::create()),
+ channel_(kGpgFrontendDefaultChannel) {
+ ui_->setupUi(this);
+
+ 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<int>(&QComboBox::currentIndexChanged), this,
+ [=](int index) {
+ channel_ = index;
+ ui_->cardKeysTreeView->SetChannel(channel_);
+ ui_->cardKeysTreeView->expandAll();
+ });
+
+ slot_refresh();
+}
+
+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;
+
+ auto line = status.front();
+ auto token = line.split(' ');
+
+ if (token.size() != 2) {
+ LOG_E() << "invalid response of command SERIALNO: " << line;
+ return;
+ }
+
+ serial_number_ = token.back().trimmed();
+ LOG_D() << "get smart card serial number: " << serial_number_;
+}
+
+void SmartCardControllerDialog::fetch_smart_card_info() {
+ if (serial_number_.isEmpty()) return;
+
+ auto [ret, status] = GpgAssuanHelper::GetInstance().SendStatusCommand(
+ GpgComponentType::kGPG_AGENT, "SCD LEARN --force " + serial_number_);
+ if (!ret || status.isEmpty()) return;
+
+ LOG_D() << "fetch card raw info: " << status;
+ card_info_ = GpgOpenPGPCard(status);
+
+ if (!card_info_.good) {
+ LOG_E() << "parse card raw info failed";
+ return;
+ }
+}
+
+void SmartCardControllerDialog::print_smart_card_info() {
+ if (!card_info_.good) return;
+
+ QString html;
+ QTextStream out(&html);
+ auto card = card_info_;
+
+ out << "<h2>" << tr("OpenPGP Card Information") << "</h2>";
+
+ out << "<h3>" << tr("Basic Information") << "</h3><ul>";
+ out << "<li><b>" << tr("Reader:") << "</b> " << card.reader << "</li>";
+ out << "<li><b>" << tr("Serial Number:") << "</b> " << card.serial_number
+ << "</li>";
+ out << "<li><b>" << tr("Card Type:") << "</b> " << card.card_type << "</li>";
+ out << "<li><b>" << tr("Card Version:") << "</b> " << card.card_version
+ << "</li>";
+ 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:") << "</b> " << card.manufacturer
+ << "</li>";
+ out << "<li><b>" << tr("Card Holder:") << "</b> " << card.card_holder
+ << "</li>";
+ out << "<li><b>" << tr("Language:") << "</b> " << card.display_language
+ << "</li>";
+ out << "<li><b>" << tr("Sex:") << "</b> " << card.display_sex << "</li>";
+ 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 << "</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 << "</ul>";
+
+ if (!card.card_infos.isEmpty()) {
+ out << "<h3>" << tr("Additional Info") << "</h3><ul>";
+ for (auto it = card.card_infos.begin(); it != card.card_infos.end(); ++it) {
+ out << "<li><b>" << tr("%1:").arg(it.key()) << "</b> " << it.value()
+ << "</li>";
+ }
+ out << "</ul>";
+ }
+
+ ui_->cardInfoEdit->setText(html);
+}
+
+void SmartCardControllerDialog::slot_refresh() {
+ ui_->cardInfoEdit->clear();
+
+ get_smart_card_serial_number();
+ fetch_smart_card_info();
+
+ print_smart_card_info();
+ refresh_key_tree_view();
+}
+
+void SmartCardControllerDialog::refresh_key_tree_view() {
+ if (card_info_.fprs.isEmpty()) {
+ ui_->cardKeysTreeView->SetKeyFilter([](auto) { return false; });
+ return;
+ }
+
+ QStringList card_fprs(card_info_.fprs.begin(), card_info_.fprs.end());
+ ui_->cardKeysTreeView->SetKeyFilter([=](const GpgAbstractKey* k) {
+ return card_fprs.contains(k->Fingerprint());
+ });
+}
+
+} // namespace GpgFrontend::UI
diff --git a/src/ui/dialog/controller/SmartCardControllerDialog.h b/src/ui/dialog/controller/SmartCardControllerDialog.h
new file mode 100644
index 00000000..9b71bba7
--- /dev/null
+++ b/src/ui/dialog/controller/SmartCardControllerDialog.h
@@ -0,0 +1,85 @@
+/**
+ * Copyright (C) 2021-2024 Saturneric <[email protected]>
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ *
+ * 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 <[email protected]> starting on May 12, 2021.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include "core/model/GpgOpenPGPCard.h"
+#include "ui/dialog/GeneralDialog.h"
+
+class Ui_SmartCardControllerDialog;
+
+namespace GpgFrontend::UI {
+class SmartCardControllerDialog : public GeneralDialog {
+ Q_OBJECT
+ public:
+ /**
+ * @brief Construct a new Smart Card Controller Dialog object
+ *
+ * @param parent
+ */
+ explicit SmartCardControllerDialog(QWidget* parent = nullptr);
+
+ private slots:
+
+ /**
+ * @brief
+ *
+ */
+ void slot_refresh();
+
+ private:
+ QSharedPointer<Ui_SmartCardControllerDialog> ui_; ///<
+ int channel_;
+ QString serial_number_;
+ GpgOpenPGPCard card_info_;
+
+ /**
+ * @brief Get the smart card serial number object
+ *
+ */
+ void get_smart_card_serial_number();
+
+ /**
+ * @brief
+ *
+ */
+ void fetch_smart_card_info();
+
+ /**
+ * @brief
+ *
+ */
+ void print_smart_card_info();
+
+ /**
+ * @brief
+ *
+ */
+ void refresh_key_tree_view();
+};
+} // namespace GpgFrontend::UI
diff --git a/src/ui/dialog/keypair_details/KeyPairSubkeyTab.cpp b/src/ui/dialog/keypair_details/KeyPairSubkeyTab.cpp
index 79a28176..c4e93b62 100644
--- a/src/ui/dialog/keypair_details/KeyPairSubkeyTab.cpp
+++ b/src/ui/dialog/keypair_details/KeyPairSubkeyTab.cpp
@@ -389,7 +389,12 @@ void KeyPairSubkeyTab::slot_refresh_subkey_detail() {
: tr("Not Exists"));
// Show the situation if key in a smart card.
- card_key_label_->setText(s_key.IsCardKey() ? tr("Yes") : tr("No"));
+ auto smart_card_info = s_key.IsCardKey() ? tr("Yes") : tr("No");
+ if (s_key.IsCardKey() && !s_key.SmartCardSerialNumber().isEmpty()) {
+ smart_card_info += " ";
+ smart_card_info += "(" + s_key.SmartCardSerialNumber() + ")";
+ }
+ card_key_label_->setText(smart_card_info);
if (!s_key.IsSecretKey()) {
auto palette_expired = master_key_exist_var_label_->palette();
diff --git a/src/ui/main_window/MainWindow.h b/src/ui/main_window/MainWindow.h
index 9e5a289c..58aa406d 100644
--- a/src/ui/main_window/MainWindow.h
+++ b/src/ui/main_window/MainWindow.h
@@ -711,8 +711,10 @@ class MainWindow : public GeneralMainWindow {
QAction* clean_double_line_breaks_act_{}; ///< Action to remove double
///< line breaks
- QAction* gnupg_controller_open_act_{}; ///<
- QAction* module_controller_open_act_{}; ///<
+ QAction* gnupg_controller_open_act_{}; ///<
+ QAction* module_controller_open_act_{}; ///<
+ QAction* smart_card_controller_open_act_{}; ///<
+
QAction* clean_gpg_password_cache_act_{}; ///<
QAction* reload_components_act_{}; ///<
QAction* restart_components_act_{}; ///<
diff --git a/src/ui/main_window/MainWindowUI.cpp b/src/ui/main_window/MainWindowUI.cpp
index a586a43b..6598aede 100644
--- a/src/ui/main_window/MainWindowUI.cpp
+++ b/src/ui/main_window/MainWindowUI.cpp
@@ -29,9 +29,10 @@
#include "MainWindow.h"
#include "core/function/GlobalSettingStation.h"
#include "core/module/ModuleManager.h"
-#include "dialog/controller/ModuleControllerDialog.h"
#include "ui/UserInterfaceUtils.h"
#include "ui/dialog/controller/GnuPGControllerDialog.h"
+#include "ui/dialog/controller/ModuleControllerDialog.h"
+#include "ui/dialog/controller/SmartCardControllerDialog.h"
#include "ui/dialog/help/AboutDialog.h"
#include "ui/widgets/KeyList.h"
#include "ui/widgets/TextEdit.h"
@@ -255,6 +256,12 @@ void MainWindow::create_actions() {
connect(module_controller_open_act_, &QAction::triggered, this,
[this]() { (new ModuleControllerDialog(this))->exec(); });
+ smart_card_controller_open_act_ = create_action(
+ "smart_card_controller_open", tr("Open Smart Card Controller"),
+ ":/icons/smart-card.png", tr("Open Smart Card Controller Dialog"));
+ connect(smart_card_controller_open_act_, &QAction::triggered, this,
+ [this]() { (new SmartCardControllerDialog(this))->exec(); });
+
/**
* E-Mail Menu
*/
@@ -462,6 +469,7 @@ void MainWindow::create_menus() {
advance_menu_->addSeparator();
advance_menu_->addAction(gnupg_controller_open_act_);
advance_menu_->addAction(module_controller_open_act_);
+ advance_menu_->addAction(smart_card_controller_open_act_);
view_menu_ = menuBar()->addMenu(tr("View"));
diff --git a/src/ui/model/GpgKeyTreeProxyModel.cpp b/src/ui/model/GpgKeyTreeProxyModel.cpp
index e58cee88..2f46f548 100644
--- a/src/ui/model/GpgKeyTreeProxyModel.cpp
+++ b/src/ui/model/GpgKeyTreeProxyModel.cpp
@@ -125,4 +125,9 @@ void GpgKeyTreeProxyModel::slot_update_favorites_cache() {
}
}
+void GpgKeyTreeProxyModel::SetKeyFilter(const KeyFilter &filter) {
+ custom_filter_ = filter;
+ invalidateFilter();
+}
+
} // namespace GpgFrontend::UI \ No newline at end of file
diff --git a/src/ui/model/GpgKeyTreeProxyModel.h b/src/ui/model/GpgKeyTreeProxyModel.h
index 5e75dd84..f41afd4f 100644
--- a/src/ui/model/GpgKeyTreeProxyModel.h
+++ b/src/ui/model/GpgKeyTreeProxyModel.h
@@ -40,14 +40,39 @@ class GpgKeyTreeProxyModel : public QSortFilterProxyModel {
public:
using KeyFilter = std::function<bool(const GpgAbstractKey *)>;
+ /**
+ * @brief Construct a new Gpg Key Tree Proxy Model object
+ *
+ * @param model
+ * @param display_mode
+ * @param filter
+ * @param parent
+ */
explicit GpgKeyTreeProxyModel(QSharedPointer<GpgKeyTreeModel> model,
GpgKeyTreeDisplayMode display_mode,
KeyFilter filter, QObject *parent);
+ /**
+ * @brief Set the Search Keywords object
+ *
+ * @param keywords
+ */
void SetSearchKeywords(const QString &keywords);
+ /**
+ * @brief
+ *
+ * @param model
+ */
void ResetGpgKeyTableModel(QSharedPointer<GpgKeyTreeModel> model);
+ /**
+ * @brief Set the Key Filter object
+ *
+ * @param filter
+ */
+ void SetKeyFilter(const KeyFilter &filter);
+
protected:
[[nodiscard]] auto filterAcceptsRow(
int sourceRow, const QModelIndex &sourceParent) const -> bool override;
diff --git a/src/ui/widgets/KeyTreeView.cpp b/src/ui/widgets/KeyTreeView.cpp
index 3dde069a..3df5a440 100644
--- a/src/ui/widgets/KeyTreeView.cpp
+++ b/src/ui/widgets/KeyTreeView.cpp
@@ -31,10 +31,24 @@
#include <utility>
#include "core/function/gpg/GpgKeyGetter.h"
-#include "model/GpgKeyTreeProxyModel.h"
+#include "core/utils/GpgUtils.h"
+#include "ui/dialog/keypair_details/KeyDetailsDialog.h"
+#include "ui/model/GpgKeyTreeProxyModel.h"
namespace GpgFrontend::UI {
+KeyTreeView::KeyTreeView(QWidget* parent)
+ : QTreeView(parent),
+ channel_(kGpgFrontendDefaultChannel),
+ model_(QSharedPointer<GpgKeyTreeModel>::create(
+ channel_, GpgKeyGetter::GetInstance(channel_).FetchKey(),
+ [](auto) { return false; }, this)),
+ proxy_model_(
+ model_, GpgKeyTreeDisplayMode::kALL, [](auto) { return false; },
+ this) {
+ init();
+}
+
KeyTreeView::KeyTreeView(int channel,
GpgKeyTreeModel::Detector checkable_detector,
GpgKeyTreeProxyModel::KeyFilter filter,
@@ -42,10 +56,37 @@ KeyTreeView::KeyTreeView(int channel,
: QTreeView(parent),
channel_(channel),
model_(QSharedPointer<GpgKeyTreeModel>::create(
- channel, GpgKeyGetter::GetInstance(channel_).FetchKey(),
+ channel_, GpgKeyGetter::GetInstance(channel_).FetchKey(),
checkable_detector, this)),
proxy_model_(model_, GpgKeyTreeDisplayMode::kALL, std::move(filter),
this) {
+ init();
+}
+
+void KeyTreeView::paintEvent(QPaintEvent* event) {
+ QTreeView::paintEvent(event);
+
+ if (!init_) {
+ slot_adjust_column_widths();
+ init_ = true;
+ }
+}
+
+void KeyTreeView::slot_adjust_column_widths() {
+ for (int i = 1; i < model_->columnCount({}); ++i) {
+ this->resizeColumnToContents(i);
+ }
+}
+
+auto KeyTreeView::GetAllCheckedKeyIds() -> KeyIdArgsList {
+ return model_->GetAllCheckedKeyIds();
+}
+
+auto KeyTreeView::GetAllCheckedSubKey() -> QContainer<GpgSubKey> {
+ return model_->GetAllCheckedSubKey();
+}
+
+void KeyTreeView::init() {
setModel(&proxy_model_);
sortByColumn(2, Qt::AscendingOrder);
@@ -64,29 +105,39 @@ KeyTreeView::KeyTreeView(int channel,
setFocusPolicy(Qt::NoFocus);
setAlternatingRowColors(true);
setSortingEnabled(true);
-}
-void KeyTreeView::paintEvent(QPaintEvent* event) {
- QTreeView::paintEvent(event);
+ connect(this, &QTableView::doubleClicked, this,
+ [this](const QModelIndex& index) {
+ if (!index.isValid() || index.column() == 0) return;
- if (!init_) {
- slot_adjust_column_widths();
- init_ = true;
- }
-}
+ QModelIndex source_index = proxy_model_.mapToSource(index);
+ auto key =
+ GetGpgKeyByGpgAbstractKey(model_->GetKeyByIndex(source_index));
-void KeyTreeView::slot_adjust_column_widths() {
- for (int i = 1; i < model_->columnCount({}); ++i) {
- this->resizeColumnToContents(i);
- }
+ if (!key.IsGood()) {
+ QMessageBox::critical(this, tr("Error"), tr("Key Not Found."));
+ return;
+ }
+
+ new KeyDetailsDialog(model_->GetGpgContextChannel(), key, this);
+ });
}
-auto KeyTreeView::GetAllCheckedKeyIds() -> KeyIdArgsList {
- return model_->GetAllCheckedKeyIds();
+void KeyTreeView::SetKeyFilter(const GpgKeyTreeProxyModel::KeyFilter& filter) {
+ proxy_model_.SetKeyFilter(filter);
}
-auto KeyTreeView::GetAllCheckedSubKey() -> QContainer<GpgSubKey> {
- return model_->GetAllCheckedSubKey();
+void KeyTreeView::SetChannel(int channel) {
+ if (channel_ == channel) return;
+ LOG_D() << "new channel for key tree view: " << channel;
+
+ channel_ = channel;
+ init_ = false;
+ model_ = QSharedPointer<GpgKeyTreeModel>::create(
+ channel_, GpgKeyGetter::GetInstance(channel_).FetchKey(),
+ [](auto) { return false; }, this);
+ proxy_model_.setSourceModel(model_.get());
+ proxy_model_.invalidate();
}
} // namespace GpgFrontend::UI
diff --git a/src/ui/widgets/KeyTreeView.h b/src/ui/widgets/KeyTreeView.h
index a362a8a1..0e12ad0b 100644
--- a/src/ui/widgets/KeyTreeView.h
+++ b/src/ui/widgets/KeyTreeView.h
@@ -48,6 +48,16 @@ class KeyTreeView : public QTreeView {
* @param _info_type
* @param _filter
*/
+ explicit KeyTreeView(QWidget* parent = nullptr);
+
+ /**
+ * @brief Construct a new Key Table object
+ *
+ * @param _key_list
+ * @param _select_type
+ * @param _info_type
+ * @param _filter
+ */
explicit KeyTreeView(int channel,
GpgKeyTreeModel::Detector checkable_detector,
GpgKeyTreeProxyModel::KeyFilter filter,
@@ -67,6 +77,20 @@ class KeyTreeView : public QTreeView {
*/
auto GetAllCheckedSubKey() -> QContainer<GpgSubKey>;
+ /**
+ * @brief Set the Key Filter object
+ *
+ * @param filter
+ */
+ void SetKeyFilter(const GpgKeyTreeProxyModel::KeyFilter& filter);
+
+ /**
+ * @brief
+ *
+ * @param channel
+ */
+ void SetChannel(int channel);
+
protected:
/**
* @brief
@@ -81,6 +105,8 @@ class KeyTreeView : public QTreeView {
GpgKeyTreeProxyModel proxy_model_;
void slot_adjust_column_widths();
+
+ void init();
};
} // namespace GpgFrontend::UI \ No newline at end of file
diff --git a/ui/SmartCardControllerDialog.ui b/ui/SmartCardControllerDialog.ui
new file mode 100644
index 00000000..537862fc
--- /dev/null
+++ b/ui/SmartCardControllerDialog.ui
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>SmartCardControllerDialog</class>
+ <widget class="QDialog" name="SmartCardControllerDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>858</width>
+ <height>571</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>SmartCardController</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <item>
+ <widget class="QTextEdit" name="cardInfoEdit">
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="GpgFrontend::UI::KeyTreeView" name="cardKeysTreeView">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Operations</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QPushButton" name="pushButton">
+ <property name="text">
+ <string>Change Name of Card Holder</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="pushButton_3">
+ <property name="text">
+ <string>Move Key to Card</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Orientation::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Orientation::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QComboBox" name="keyDBIndexComboBox"/>
+ </item>
+ <item>
+ <widget class="QPushButton" name="refreshButton">
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>GpgFrontend::UI::KeyTreeView</class>
+ <extends>QTreeView</extends>
+ <header>ui/widgets/KeyTreeView.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>