aboutsummaryrefslogtreecommitdiffstats
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
parentfeat: add openpgp smart card support (diff)
downloadGpgFrontend-7ca18eb0e2c4204f749c682b66c862968e8d2f58.tar.gz
GpgFrontend-7ca18eb0e2c4204f749c682b66c862968e8d2f58.zip
feat: add SmartCardController
-rw-r--r--src/core/function/gpg/GpgAdvancedOperator.cpp7
-rw-r--r--src/core/function/gpg/GpgAssuanHelper.cpp24
-rw-r--r--src/core/function/gpg/GpgAssuanHelper.h75
-rw-r--r--src/core/function/gpg/GpgAutomatonHandler.cpp25
-rw-r--r--src/core/function/gpg/GpgAutomatonHandler.h38
-rw-r--r--src/core/function/gpg/GpgCommandExecutor.cpp32
-rw-r--r--src/core/function/gpg/GpgCommandExecutor.h13
-rw-r--r--src/core/function/gpg/GpgContext.cpp4
-rw-r--r--src/core/function/gpg/GpgSmartCardManager.cpp87
-rw-r--r--src/core/function/gpg/GpgSmartCardManager.h (renamed from src/core/model/GpgCardKeyPairInfo.h)48
-rw-r--r--src/core/model/GpgCardKeyPairInfo.cpp73
-rw-r--r--src/core/model/GpgOpenPGPCard.cpp172
-rw-r--r--src/core/model/GpgOpenPGPCard.h86
-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
-rw-r--r--ui/SmartCardControllerDialog.ui120
21 files changed, 1236 insertions, 204 deletions
diff --git a/src/core/function/gpg/GpgAdvancedOperator.cpp b/src/core/function/gpg/GpgAdvancedOperator.cpp
index 0b103c9b..492297c6 100644
--- a/src/core/function/gpg/GpgAdvancedOperator.cpp
+++ b/src/core/function/gpg/GpgAdvancedOperator.cpp
@@ -54,13 +54,18 @@ void ExecuteGpgCommand(const QString &operation, const QStringList &extra_args,
std::atomic<int> completed_tasks{0};
std::vector<int> results(total_tasks, 0);
+ // kill default gpg-agent
+ key_dbs.push_back({});
+
int task_index = 0;
for (const auto &key_db : key_dbs) {
const int current_index = task_index++;
const auto target_home_dir =
QDir::toNativeSeparators(QFileInfo(key_db.path).canonicalFilePath());
- QStringList arguments = QStringList{"--homedir", target_home_dir};
+ QStringList arguments = !target_home_dir.isEmpty()
+ ? QStringList{"--homedir", target_home_dir}
+ : QStringList{};
arguments.append(extra_args);
GpgCommandExecutor::ExecuteSync(
diff --git a/src/core/function/gpg/GpgAssuanHelper.cpp b/src/core/function/gpg/GpgAssuanHelper.cpp
index 2351b9a2..8fa311dc 100644
--- a/src/core/function/gpg/GpgAssuanHelper.cpp
+++ b/src/core/function/gpg/GpgAssuanHelper.cpp
@@ -51,7 +51,7 @@ auto GpgAssuanHelper::ConnectToSocket(GpgComponentType type) -> bool {
auto socket_path = ctx_.ComponentDirectory(type);
if (socket_path.isEmpty()) {
- LOG_F() << "socket path of component: " << component_type_to_q_string(type)
+ LOG_W() << "socket path of component: " << component_type_to_q_string(type)
<< " is empty";
return false;
}
@@ -65,7 +65,7 @@ auto GpgAssuanHelper::ConnectToSocket(GpgComponentType type) -> bool {
launch_component(type);
if (!info.exists()) {
- LOG_F() << "socket path is still not exists: " << socket_path
+ LOG_W() << "socket path is still not exists: " << socket_path
<< "abort...";
return false;
}
@@ -77,17 +77,17 @@ auto GpgAssuanHelper::ConnectToSocket(GpgComponentType type) -> bool {
auto err = assuan_socket_connect(a_ctx, info.absoluteFilePath().toUtf8(),
ASSUAN_INVALID_PID, 0);
if (err != GPG_ERR_NO_ERROR) {
- LOG_F() << "failed to connect to socket:" << CheckGpgError(err);
+ LOG_W() << "failed to connect to socket:" << CheckGpgError(err);
return false;
}
LOG_D() << "connected to socket by assuan protocol: "
- << info.absoluteFilePath();
+ << info.absoluteFilePath() << "channel:" << GetChannel();
err = assuan_transact(a_ctx, "GETINFO pid", simple_data_callback, nullptr,
nullptr, nullptr, nullptr, nullptr);
if (err != GPG_ERR_NO_ERROR) {
- LOG_F() << "failed to test assuan connection:" << CheckGpgError(err);
+ LOG_W() << "failed to test assuan connection:" << CheckGpgError(err);
return false;
}
@@ -111,12 +111,19 @@ auto GpgAssuanHelper::SendCommand(GpgComponentType type, const QString& command,
context->status_cb = std::move(status_cb);
context->inquery_cb = std::move(inquery_cb);
+ LOG_D() << "sending assuan command: " << command;
+
auto err = assuan_transact(
assuan_ctx_[type], command.toUtf8(), default_data_callback, &context,
default_inquery_callback, &context, default_status_callback, &context);
if (err != GPG_ERR_NO_ERROR) {
- LOG_F() << "failed to send assuan command :" << CheckGpgError(err);
+ LOG_W() << "failed to send assuan command:" << CheckGpgError(err);
+
+ // broken pipe error, try reconnect next time
+ if (CheckGpgError(err) == 32877) {
+ assuan_ctx_.remove(type);
+ }
return false;
}
@@ -153,7 +160,6 @@ auto GpgAssuanHelper::SendStatusCommand(GpgComponentType type,
};
auto ret = SendCommand(type, command, d_cb, i_cb, s_cb);
-
return {ret, status_lines};
}
@@ -185,7 +191,7 @@ auto GpgAssuanHelper::default_inquery_callback(
void GpgAssuanHelper::launch_component(GpgComponentType type) {
if (gpgconf_path_.isEmpty()) {
- LOG_F() << "gpgconf_path is not collected by initializing";
+ LOG_W() << "gpgconf_path is not collected by initializing";
return;
}
@@ -199,7 +205,7 @@ void GpgAssuanHelper::launch_component(GpgComponentType type) {
process.start();
if (!process.waitForFinished()) {
- LOG_F() << "failed to execute gpgconf" << process.arguments();
+ LOG_E() << "failed to execute gpgconf" << process.arguments();
return;
}
}
diff --git a/src/core/function/gpg/GpgAssuanHelper.h b/src/core/function/gpg/GpgAssuanHelper.h
index 65ec325f..6e58e27c 100644
--- a/src/core/function/gpg/GpgAssuanHelper.h
+++ b/src/core/function/gpg/GpgAssuanHelper.h
@@ -62,15 +62,49 @@ class GPGFRONTEND_CORE_EXPORT GpgAssuanHelper
[[nodiscard]] auto SendData(const QByteArray& b) const -> gpg_error_t;
};
+ /**
+ * @brief Construct a new Gpg Assuan Helper object
+ *
+ * @param channel
+ */
explicit GpgAssuanHelper(int channel);
+
+ /**
+ * @brief Destroy the Gpg Assuan Helper object
+ *
+ */
~GpgAssuanHelper();
+ /**
+ * @brief
+ *
+ * @return true
+ * @return false
+ */
auto ConnectToSocket(GpgComponentType) -> bool;
+ /**
+ * @brief
+ *
+ * @param type
+ * @param command
+ * @param data_cb
+ * @param inquery_cb
+ * @param status_cb
+ * @return true
+ * @return false
+ */
auto SendCommand(GpgComponentType type, const QString& command,
DataCallback data_cb, InqueryCallback inquery_cb,
StatusCallback status_cb) -> bool;
+ /**
+ * @brief
+ *
+ * @param type
+ * @param command
+ * @return std::tuple<bool, QStringList>
+ */
auto SendStatusCommand(GpgComponentType type, const QString& command)
-> std::tuple<bool, QStringList>;
@@ -79,19 +113,60 @@ class GPGFRONTEND_CORE_EXPORT GpgAssuanHelper
GpgContext::GetInstance(SingletonFunctionObject::GetChannel());
QMap<GpgComponentType, assuan_context_t> assuan_ctx_;
+ /**
+ * @brief
+ *
+ * @param type
+ */
void launch_component(GpgComponentType type);
+ /**
+ * @brief
+ *
+ * @param type
+ * @return QString
+ */
static auto component_type_to_q_string(GpgComponentType type) -> QString;
+ /**
+ * @brief
+ *
+ * @param opaque
+ * @param buffer
+ * @param length
+ * @return gpgme_error_t
+ */
static auto simple_data_callback(void* opaque, const void* buffer,
size_t length) -> gpgme_error_t;
+ /**
+ * @brief
+ *
+ * @param opaque
+ * @param buffer
+ * @param length
+ * @return gpgme_error_t
+ */
static auto default_data_callback(void* opaque, const void* buffer,
size_t length) -> gpgme_error_t;
+ /**
+ * @brief
+ *
+ * @param opaque
+ * @param status
+ * @return gpgme_error_t
+ */
static auto default_status_callback(void* opaque,
const char* status) -> gpgme_error_t;
+ /**
+ * @brief
+ *
+ * @param opaque
+ * @param inquery
+ * @return gpgme_error_t
+ */
static auto default_inquery_callback(void* opaque,
const char* inquery) -> gpgme_error_t;
diff --git a/src/core/function/gpg/GpgAutomatonHandler.cpp b/src/core/function/gpg/GpgAutomatonHandler.cpp
index 802279ed..af2f0cba 100644
--- a/src/core/function/gpg/GpgAutomatonHandler.cpp
+++ b/src/core/function/gpg/GpgAutomatonHandler.cpp
@@ -28,6 +28,8 @@
#include "GpgAutomatonHandler.h"
+#include <utility>
+
#include "core/model/GpgData.h"
#include "core/model/GpgKey.h"
#include "core/utils/GpgUtils.h"
@@ -96,21 +98,30 @@ auto GpgAutomatonHandler::interator_cb_func(void* handle, const char* status,
auto GpgAutomatonHandler::DoInteract(
const GpgKey& key, AutomatonNextStateHandler next_state_handler,
- AutomatonActionHandler action_handler) -> bool {
- auto key_fpr = key.Fingerprint();
- AutomatonHandelStruct handel_struct(key_fpr);
+ AutomatonActionHandler action_handler, int flags) -> bool {
+ gpgme_key_t p_key =
+ flags == GPGME_INTERACT_CARD ? nullptr : static_cast<gpgme_key_t>(key);
+
+ AutomatonHandelStruct handel_struct(
+ flags == GPGME_INTERACT_CARD ? "" : key.Fingerprint());
handel_struct.SetHandler(std::move(next_state_handler),
std::move(action_handler));
GpgData data_out;
- auto err =
- gpgme_op_interact(ctx_.DefaultContext(), static_cast<gpgme_key_t>(key), 0,
- GpgAutomatonHandler::interator_cb_func,
- static_cast<void*>(&handel_struct), data_out);
+ auto err = gpgme_op_interact(ctx_.DefaultContext(), p_key, flags,
+ GpgAutomatonHandler::interator_cb_func,
+ static_cast<void*>(&handel_struct), data_out);
return CheckGpgError(err) == GPG_ERR_NO_ERROR && handel_struct.Success();
}
+auto GpgAutomatonHandler::DoCardInteract(
+ AutomatonNextStateHandler next_state_handler,
+ AutomatonActionHandler action_handler) -> bool {
+ return DoInteract({}, std::move(next_state_handler),
+ std::move(action_handler), GPGME_INTERACT_CARD);
+}
+
auto GpgAutomatonHandler::AutomatonHandelStruct::NextState(
QString gpg_status, QString args) -> AutomatonState {
return next_state_handler_(current_state_, std::move(gpg_status),
diff --git a/src/core/function/gpg/GpgAutomatonHandler.h b/src/core/function/gpg/GpgAutomatonHandler.h
index 78b20252..f86299e8 100644
--- a/src/core/function/gpg/GpgAutomatonHandler.h
+++ b/src/core/function/gpg/GpgAutomatonHandler.h
@@ -87,16 +87,46 @@ class GpgAutomatonHandler
explicit GpgAutomatonHandler(
int channel = SingletonFunctionObject::GetDefaultChannel());
+ /**
+ * @brief
+ *
+ * @param key
+ * @param next_state_handler
+ * @param action_handler
+ * @param flags
+ * @return true
+ * @return false
+ */
auto DoInteract(const GpgKey& key,
AutomatonNextStateHandler next_state_handler,
- AutomatonActionHandler action_handler) -> bool;
+ AutomatonActionHandler action_handler, int flags = 0) -> bool;
- private:
- static auto interator_cb_func(void* handle, const char* status,
- const char* args, int fd) -> gpgme_error_t;
+ /**
+ * @brief
+ *
+ * @param next_state_handler
+ * @param action_handler
+ * @return true
+ * @return false
+ */
+ auto DoCardInteract(AutomatonNextStateHandler next_state_handler,
+ AutomatonActionHandler action_handler) -> bool;
+ private:
GpgContext& ctx_ =
GpgContext::GetInstance(SingletonFunctionObject::GetChannel()); ///<
+
+ /**
+ * @brief
+ *
+ * @param handle
+ * @param status
+ * @param args
+ * @param fd
+ * @return gpgme_error_t
+ */
+ static auto interator_cb_func(void* handle, const char* status,
+ const char* args, int fd) -> gpgme_error_t;
};
using AutomatonNextStateHandler =
diff --git a/src/core/function/gpg/GpgCommandExecutor.cpp b/src/core/function/gpg/GpgCommandExecutor.cpp
index a4ab8990..191c1259 100644
--- a/src/core/function/gpg/GpgCommandExecutor.cpp
+++ b/src/core/function/gpg/GpgCommandExecutor.cpp
@@ -31,6 +31,7 @@
#include "core/model/DataObject.h"
#include "core/module/Module.h"
+#include "core/module/ModuleManager.h"
#include "core/thread/Task.h"
#include "core/thread/TaskRunnerGetter.h"
@@ -234,4 +235,35 @@ GpgCommandExecutor::ExecuteContext::ExecuteContext(
int_func(std::move(int_func)),
task_runner(std::move(task_runner)) {}
+GpgCommandExecutor::GpgCommandExecutor(int channel)
+ : GpgFrontend::SingletonFunctionObject<GpgCommandExecutor>(channel) {}
+
+void GpgCommandExecutor::GpgExecuteSync(const ExecuteContext &context) {
+ const auto gpg_path = Module::RetrieveRTValueTypedOrDefault<>(
+ "core", "gpgme.ctx.app_path", QString{});
+
+ if (context.cmd.isEmpty() && gpg_path.isEmpty()) {
+ LOG_E() << "failed to execute gpg command, gpg binary path is empty.";
+ return;
+ }
+
+ LOG_D() << "got gpg binary path:" << gpg_path;
+ LOG_D() << "context channel:" << GetChannel()
+ << "home path: " << ctx_.HomeDirectory();
+
+ ExecuteContext ctx = {
+ context.cmd.isEmpty() ? gpg_path : context.cmd,
+ context.arguments,
+ context.cb_func,
+ context.task_runner,
+ context.int_func,
+ };
+
+ if (!ctx.arguments.contains("--homedir") && !ctx_.HomeDirectory().isEmpty()) {
+ ctx.arguments.append("--homedir");
+ ctx.arguments.append(ctx_.HomeDirectory());
+ }
+
+ return ExecuteSync(ctx);
+}
} // namespace GpgFrontend \ No newline at end of file
diff --git a/src/core/function/gpg/GpgCommandExecutor.h b/src/core/function/gpg/GpgCommandExecutor.h
index 5a2f13db..fd2181d3 100644
--- a/src/core/function/gpg/GpgCommandExecutor.h
+++ b/src/core/function/gpg/GpgCommandExecutor.h
@@ -28,6 +28,8 @@
#pragma once
+#include "core/function/basic/GpgFunctionObject.h"
+#include "core/function/gpg/GpgContext.h"
#include "core/module/Module.h"
namespace GpgFrontend {
@@ -39,7 +41,8 @@ using GpgCommandExecutorInterator = std::function<void(QProcess *)>;
* @brief Extra commands related to GPG
*
*/
-class GPGFRONTEND_CORE_EXPORT GpgCommandExecutor {
+class GPGFRONTEND_CORE_EXPORT GpgCommandExecutor
+ : public SingletonFunctionObject<GpgCommandExecutor> {
public:
struct GPGFRONTEND_CORE_EXPORT ExecuteContext {
QString cmd;
@@ -58,6 +61,8 @@ class GPGFRONTEND_CORE_EXPORT GpgCommandExecutor {
using ExecuteContexts = QContainer<ExecuteContext>;
+ explicit GpgCommandExecutor(int channel = kGpgFrontendDefaultChannel);
+
/**
* @brief Excuting a command
*
@@ -69,6 +74,12 @@ class GPGFRONTEND_CORE_EXPORT GpgCommandExecutor {
static void ExecuteConcurrentlyAsync(ExecuteContexts);
static void ExecuteConcurrentlySync(ExecuteContexts);
+
+ void GpgExecuteSync(const ExecuteContext &);
+
+ private:
+ GpgContext &ctx_ =
+ GpgContext::GetInstance(SingletonFunctionObject::GetChannel());
};
} // namespace GpgFrontend
diff --git a/src/core/function/gpg/GpgContext.cpp b/src/core/function/gpg/GpgContext.cpp
index ce84ed2e..88eb3c1b 100644
--- a/src/core/function/gpg/GpgContext.cpp
+++ b/src/core/function/gpg/GpgContext.cpp
@@ -386,11 +386,11 @@ class GpgContext::Impl {
QProcess process;
process.setProgram(gpgconf_path);
- process.setArguments({"--list-dirs"});
+ process.setArguments({"--list-dirs", "--homedir", database_path_});
process.start();
if (!process.waitForFinished()) {
- LOG_F() << "failed to execute gpgconf --list-dirs";
+ LOG_W() << "failed to execute gpgconf --list-dirs";
return;
}
diff --git a/src/core/function/gpg/GpgSmartCardManager.cpp b/src/core/function/gpg/GpgSmartCardManager.cpp
new file mode 100644
index 00000000..06010756
--- /dev/null
+++ b/src/core/function/gpg/GpgSmartCardManager.cpp
@@ -0,0 +1,87 @@
+/**
+ * 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 "GpgSmartCardManager.h"
+
+#include "core/function/gpg/GpgAutomatonHandler.h"
+
+namespace GpgFrontend {
+
+GpgSmartCardManager::GpgSmartCardManager(int channel)
+ : SingletonFunctionObject<GpgSmartCardManager>(channel) {}
+
+auto GpgSmartCardManager::Fetch(const QString& serial_number) -> bool {
+ GpgAutomatonHandler::AutomatonNextStateHandler next_state_handler =
+ [=](AutomatonState state, QString status, QString args) {
+ auto tokens = args.split(' ');
+
+ switch (state) {
+ case GpgAutomatonHandler::AS_START:
+ if (status == "CARDCTRL" && args.contains(serial_number)) {
+ return GpgAutomatonHandler::AS_START;
+ } else if (status == "GET_LINE" && args == "cardedit.prompt") {
+ return GpgAutomatonHandler::AS_COMMAND;
+ }
+ return GpgAutomatonHandler::AS_ERROR;
+ case GpgAutomatonHandler::AS_COMMAND:
+ if (status == "GET_LINE" && args == "cardedit.prompt") {
+ return GpgAutomatonHandler::AS_QUIT;
+ }
+ return GpgAutomatonHandler::AS_ERROR;
+ case GpgAutomatonHandler::AS_QUIT:
+ case GpgAutomatonHandler::AS_ERROR:
+ if (status == "GET_LINE" && args == "keyedit.prompt") {
+ return GpgAutomatonHandler::AS_QUIT;
+ }
+ return GpgAutomatonHandler::AS_ERROR;
+ default:
+ return GpgAutomatonHandler::AS_ERROR;
+ };
+ };
+
+ AutomatonActionHandler action_handler = [](AutomatonHandelStruct& handler,
+ AutomatonState state) {
+ switch (state) {
+ case GpgAutomatonHandler::AS_COMMAND:
+ return QString("fetch");
+ case GpgAutomatonHandler::AS_QUIT:
+ return QString("quit");
+ case GpgAutomatonHandler::AS_START:
+ case GpgAutomatonHandler::AS_ERROR:
+ return QString("");
+ default:
+ return QString("");
+ }
+ return QString("");
+ };
+
+ return GpgAutomatonHandler::GetInstance(GetChannel())
+ .DoCardInteract(next_state_handler, action_handler);
+}
+
+} // namespace GpgFrontend \ No newline at end of file
diff --git a/src/core/model/GpgCardKeyPairInfo.h b/src/core/function/gpg/GpgSmartCardManager.h
index 132ded84..9c8cc8bb 100644
--- a/src/core/model/GpgCardKeyPairInfo.h
+++ b/src/core/function/gpg/GpgSmartCardManager.h
@@ -28,21 +28,47 @@
#pragma once
+#include "core/function/basic/GpgFunctionObject.h"
+#include "core/function/gpg/GpgContext.h"
+#include "core/typedef/GpgTypedef.h"
+
namespace GpgFrontend {
-struct GpgCardKeyPairInfo {
- explicit GpgCardKeyPairInfo(const QString &status);
+/**
+ * @brief
+ *
+ */
+class GPGFRONTEND_CORE_EXPORT GpgSmartCardManager
+ : public SingletonFunctionObject<GpgSmartCardManager> {
+ public:
+ /**
+ * @brief Construct a new Gpg Key Manager object
+ *
+ * @param channel
+ */
+ explicit GpgSmartCardManager(
+ int channel = SingletonFunctionObject::GetDefaultChannel());
+
+ /**
+ * @brief
+ *
+ * @param key
+ * @param subkey_index
+ * @return true
+ * @return false
+ */
+ auto Fetch(const QString& serial_number) -> bool;
- [[nodiscard]] auto CanAuthenticate() const -> bool;
- [[nodiscard]] auto CanCertify() const -> bool;
- [[nodiscard]] auto CanEncrypt() const -> bool;
- [[nodiscard]] auto CanSign() const -> bool;
+ /**
+ * @brief
+ *
+ * @return std::tuple<bool, QString>
+ */
+ auto ModifyAttr() -> std::tuple<bool, QString>;
- QString key_ref;
- QString grip;
- QString usage;
- QDateTime time;
- QString algorithm;
+ private:
+ GpgContext& ctx_ =
+ GpgContext::GetInstance(SingletonFunctionObject::GetChannel()); ///<
};
} // namespace GpgFrontend
diff --git a/src/core/model/GpgCardKeyPairInfo.cpp b/src/core/model/GpgCardKeyPairInfo.cpp
deleted file mode 100644
index 481d88ea..00000000
--- a/src/core/model/GpgCardKeyPairInfo.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * 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/GpgOpenPGPCard.cpp b/src/core/model/GpgOpenPGPCard.cpp
index f0d8c9cd..ddc9a40e 100644
--- a/src/core/model/GpgOpenPGPCard.cpp
+++ b/src/core/model/GpgOpenPGPCard.cpp
@@ -28,7 +28,6 @@
#include "GpgOpenPGPCard.h"
-#include "core/model/GpgCardKeyPairInfo.h"
#include "core/utils/CommonUtils.h"
namespace GpgFrontend {
@@ -46,39 +45,33 @@ void GpgFrontend::GpgOpenPGPCard::parse_card_info(const QString& name,
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 == "KEYPAIRINFO" || name == "KEY-FPR" || name == "KEY-TIME") {
+ parse_card_key_info(name, value);
} 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());
- }
+ const auto values = value.split(QLatin1Char(' '));
+ if (values.size() < 2) return;
+ manufacturer_id = values.front().toInt();
+ manufacturer = values.back();
+ } else if (name == "DISP-SEX") {
+ display_sex = value == "1" ? "Male" : value == "2" ? "Female" : "Unknown";
+ } else if (name == "CHV-STATUS") {
+ parse_chv_status(value);
+ } else if (name == "EXTCAP") {
+ parse_ext_capability(value);
+ } else if (name == "KDF") {
+ parse_kdf_status(value);
+ } else if (name.startsWith("UIF-")) {
+ parse_uif(name, value);
} else {
- card_infos.insert(name, value);
+ additional_card_infos.insert(name, value);
}
+
+ reader = additional_card_infos.value("READER").replace('+', ' ');
+ serial_number = additional_card_infos.value("SERIALNO");
+ app_type = additional_card_infos.value("APPTYPE");
+ display_language = additional_card_infos.value("DISP-LANG");
}
+
GpgOpenPGPCard::GpgOpenPGPCard(const QStringList& status) : good(true) {
for (const QString& line : status) {
auto tokens = line.split(' ', Qt::SkipEmptyParts);
@@ -88,4 +81,123 @@ GpgOpenPGPCard::GpgOpenPGPCard(const QStringList& status) : good(true) {
parse_card_info(name, value);
}
}
+
+void GpgOpenPGPCard::parse_chv_status(const QString& value) {
+ auto tokens = value.trimmed().split('+', Qt::SkipEmptyParts);
+
+ int index = 0;
+
+ if (index < tokens.size()) chv1_cached = tokens[index++].toInt();
+
+ // chv_max_len[3]
+ for (int i = 0; i < 3 && index < tokens.size(); ++i) {
+ chv_max_len[i] = tokens[index++].toInt();
+ }
+
+ // chv_retry[3]
+ for (int i = 0; i < 3 && index < tokens.size(); ++i) {
+ chv_retry[i] = tokens[index++].toInt();
+ }
+}
+
+void GpgOpenPGPCard::parse_ext_capability(const QString& value) {
+ auto parts = value.trimmed().split("+");
+
+ for (const QString& part : parts) {
+ auto equal_pos = part.indexOf('=');
+ if (equal_pos == -1) continue;
+
+ auto key = part.left(equal_pos).trimmed();
+ auto value = part.mid(equal_pos + 1).trimmed();
+
+ bool ok = false;
+ int ivalue = value.toInt(&ok);
+
+ if (key == "ki") {
+ ext_cap.ki = (ivalue == 1);
+ } else if (key == "aac") {
+ ext_cap.aac = (ivalue == 1);
+ } else if (key == "bt") {
+ ext_cap.bt = (ivalue == 1);
+ } else if (key == "kdf") {
+ ext_cap.kdf = (ivalue == 1);
+ } else if (key == "si" && ok) {
+ ext_cap.status_indicator = ivalue;
+ }
+ }
+}
+
+void GpgOpenPGPCard::parse_kdf_status(const QString& value) {
+ auto decoded = QByteArray::fromPercentEncoding(value.toUtf8());
+
+ if (decoded.size() < 23) {
+ kdf_do_enabled = 0;
+ return;
+ }
+
+ if (static_cast<quint8>(decoded[2]) != 0x03) {
+ kdf_do_enabled = 0;
+ } else if (static_cast<quint8>(decoded[22]) != 0x85) {
+ kdf_do_enabled = 1;
+ } else {
+ kdf_do_enabled = 2;
+ }
+}
+
+void GpgOpenPGPCard::parse_uif(const QString& name, const QString& value) {
+ auto index = name.back().digitValue() - 1;
+ if (index < 0 || index > 2) return;
+
+ auto decoded = QByteArray::fromPercentEncoding(value.toUtf8());
+ bool enabled = !decoded.isEmpty() && static_cast<quint8>(decoded[0]) != 0xFF;
+
+ switch (index) {
+ case 0:
+ uif.sign = enabled;
+ break;
+ case 1:
+ uif.encrypt = enabled;
+ break;
+ case 2:
+ uif.auth = enabled;
+ break;
+ }
+}
+
+void GpgOpenPGPCard::parse_card_key_info(const QString& name,
+ const QString& value) {
+ if (name == "KEY-FPR") {
+ auto tokens = value.split(' ');
+ if (tokens.size() >= 2) {
+ int no = tokens[0].toInt();
+ card_keys_info[no].fingerprint = tokens[1].toUpper();
+ }
+ } else if (name == "KEY-TIME") {
+ auto tokens = value.split(' ');
+ if (tokens.size() >= 2) {
+ int no = tokens.front().toInt();
+ qint64 ts = tokens.back().toLongLong();
+ card_keys_info[no].created =
+ QDateTime::fromSecsSinceEpoch(ts, QTimeZone::UTC);
+ }
+ } else if (name == "KEYPAIRINFO") {
+ auto tokens = value.split(' ');
+ if (tokens.size() < 2) return;
+
+ auto key_type_tokens = tokens[1].split('.');
+ if (key_type_tokens.size() < 2) return;
+
+ int no = key_type_tokens[1].toInt();
+ card_keys_info[no].key_type = key_type_tokens[0];
+ card_keys_info[no].grip = tokens[0].toUpper();
+
+ if (tokens.size() >= 3) {
+ card_keys_info[no].usage = tokens[2].toUpper();
+ }
+
+ if (tokens.size() >= 5) {
+ card_keys_info[no].algo = tokens[4].toUpper();
+ }
+ }
+}
} // namespace GpgFrontend
diff --git a/src/core/model/GpgOpenPGPCard.h b/src/core/model/GpgOpenPGPCard.h
index 78b7d854..b037811d 100644
--- a/src/core/model/GpgOpenPGPCard.h
+++ b/src/core/model/GpgOpenPGPCard.h
@@ -28,9 +28,6 @@
#pragma once
-#include "core/model/GpgCardKeyPairInfo.h"
-#include "core/typedef/CoreTypedef.h"
-
namespace GpgFrontend {
struct GPGFRONTEND_CORE_EXPORT GpgOpenPGPCard {
@@ -41,23 +38,44 @@ struct GPGFRONTEND_CORE_EXPORT GpgOpenPGPCard {
int card_version;
QString app_type;
int app_version;
- QString ext_capability;
+ int manufacturer_id;
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;
+ struct GpgCardKeyInfo {
+ QString fingerprint;
+ QDateTime created;
+ QString grip;
+ QString key_type;
+ QString algo;
+ QString usage;
+ };
+
+ QMap<int, GpgCardKeyInfo> card_keys_info;
+
+ int kdf_do_enabled;
+ struct UIFStatus {
+ bool sign = false;
+ bool encrypt = false;
+ bool auth = false;
+ } uif;
- QString kdf;
- QString uif1;
- QString uif2;
- QString uif3;
+ int chv1_cached = -1;
+ std::array<int, 3> chv_max_len = {-1, -1, -1};
+ std::array<int, 3> chv_retry = {-1, -1, -1};
+ struct ExtCapability {
+ bool ki = false;
+ bool aac = false;
+ bool bt = false;
+ bool kdf = false;
+ int status_indicator = -1;
+ } ext_cap;
+
+ QMap<QString, QString> additional_card_infos;
bool good = false;
GpgOpenPGPCard() = default;
@@ -65,7 +83,51 @@ struct GPGFRONTEND_CORE_EXPORT GpgOpenPGPCard {
explicit GpgOpenPGPCard(const QStringList& status);
private:
+ /**
+ * @brief
+ *
+ * @param name
+ * @param value
+ */
void parse_card_info(const QString& name, const QString& value);
+
+ /**
+ * @brief
+ *
+ * @param name
+ * @param value
+ */
+ void parse_chv_status(const QString& value);
+
+ /**
+ * @brief
+ *
+ * @param value
+ */
+ void parse_ext_capability(const QString& value);
+
+ /**
+ * @brief
+ *
+ * @param value
+ */
+ void parse_kdf_status(const QString& value);
+
+ /**
+ * @brief
+ *
+ * @param name
+ * @param value
+ */
+ void parse_uif(const QString& name, const QString& value);
+
+ /**
+ * @brief
+ *
+ * @param keyword
+ * @param value
+ */
+ void parse_card_key_info(const QString& name, const QString& value);
};
} // namespace GpgFrontend \ No newline at end of file
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) {
diff --git a/ui/SmartCardControllerDialog.ui b/ui/SmartCardControllerDialog.ui
index 537862fc..36b7ace0 100644
--- a/ui/SmartCardControllerDialog.ui
+++ b/ui/SmartCardControllerDialog.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>858</width>
- <height>571</height>
+ <height>713</height>
</rect>
</property>
<property name="windowTitle">
@@ -19,6 +19,20 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Smart Card(s):</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="currentCardComboBox"/>
+ </item>
+ </layout>
+ </item>
+ <item>
<widget class="QTextEdit" name="cardInfoEdit">
<property name="readOnly">
<bool>true</bool>
@@ -26,6 +40,33 @@
</widget>
</item>
<item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Key Stub(s) in Key Database(s):</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="keyDBIndexComboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
<widget class="GpgFrontend::UI::KeyTreeView" name="cardKeysTreeView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -38,6 +79,13 @@
</layout>
</item>
<item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Orientation::Vertical</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
@@ -48,16 +96,65 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
- <widget class="QPushButton" name="pushButton">
+ <widget class="QPushButton" name="cNameButton">
<property name="text">
- <string>Change Name of Card Holder</string>
+ <string>Change Name</string>
</property>
</widget>
</item>
<item>
- <widget class="QPushButton" name="pushButton_3">
+ <widget class="QPushButton" name="cLangButton">
<property name="text">
- <string>Move Key to Card</string>
+ <string>Change Lanugage</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cGenderButton">
+ <property name="text">
+ <string>Change Gender</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cLoginDataButton">
+ <property name="text">
+ <string>Change Login Data</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cPubKeyURLButton">
+ <property name="text">
+ <string>Change Public Key URL</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="orientation">
+ <enum>Qt::Orientation::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cPINButton">
+ <property name="text">
+ <string>Change PIN</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cAdminPINButton">
+ <property name="text">
+ <string>Change Admin PIN</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="cResetCodeButton">
+ <property name="text">
+ <string>Change Reset Code</string>
</property>
</widget>
</item>
@@ -74,6 +171,13 @@
</property>
</spacer>
</item>
+ <item>
+ <widget class="QPushButton" name="fetchButton">
+ <property name="text">
+ <string>Fetch</string>
+ </property>
+ </widget>
+ </item>
</layout>
</item>
</layout>
@@ -93,7 +197,11 @@
</spacer>
</item>
<item>
- <widget class="QComboBox" name="keyDBIndexComboBox"/>
+ <widget class="QPushButton" name="restartGpgAgentButton">
+ <property name="text">
+ <string>Restart All Gpg-Agents</string>
+ </property>
+ </widget>
</item>
<item>
<widget class="QPushButton" name="refreshButton">