diff options
author | saturneric <[email protected]> | 2025-04-13 19:33:31 +0000 |
---|---|---|
committer | saturneric <[email protected]> | 2025-04-13 19:33:31 +0000 |
commit | 7ca18eb0e2c4204f749c682b66c862968e8d2f58 (patch) | |
tree | ba8e0303ebc6ec376b1be580408d9cd91f58f2d8 /src | |
parent | feat: add openpgp smart card support (diff) | |
download | GpgFrontend-7ca18eb0e2c4204f749c682b66c862968e8d2f58.tar.gz GpgFrontend-7ca18eb0e2c4204f749c682b66c862968e8d2f58.zip |
feat: add SmartCardController
Diffstat (limited to 'src')
20 files changed, 1122 insertions, 198 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) { |