diff options
author | saturneric <[email protected]> | 2025-04-12 19:49:00 +0000 |
---|---|---|
committer | saturneric <[email protected]> | 2025-04-12 19:49:00 +0000 |
commit | 5fb637cd8ebf56739177847f90a0b91c39d7d131 (patch) | |
tree | aa1bf70b07e3977a0ee7aab036daa38217db5738 /src | |
parent | fix: issues on adsk operations (diff) | |
download | GpgFrontend-5fb637cd8ebf56739177847f90a0b91c39d7d131.tar.gz GpgFrontend-5fb637cd8ebf56739177847f90a0b91c39d7d131.zip |
feat: add assuan direct comm support
Diffstat (limited to 'src')
-rw-r--r-- | src/core/function/gpg/GpgAssuanHelper.cpp | 232 | ||||
-rw-r--r-- | src/core/function/gpg/GpgAssuanHelper.h | 103 | ||||
-rw-r--r-- | src/core/function/gpg/GpgAutomatonHandler.h | 2 | ||||
-rw-r--r-- | src/core/function/gpg/GpgContext.cpp | 108 | ||||
-rw-r--r-- | src/core/function/gpg/GpgContext.h | 8 | ||||
-rw-r--r-- | src/test/core/GpgCoreTestAssuan.cpp | 76 |
6 files changed, 513 insertions, 16 deletions
diff --git a/src/core/function/gpg/GpgAssuanHelper.cpp b/src/core/function/gpg/GpgAssuanHelper.cpp new file mode 100644 index 00000000..2351b9a2 --- /dev/null +++ b/src/core/function/gpg/GpgAssuanHelper.cpp @@ -0,0 +1,232 @@ +/** + * 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 "GpgAssuanHelper.h" + +#include <utility> + +#include "core/module/ModuleManager.h" +#include "core/utils/GpgUtils.h" + +namespace GpgFrontend { + +GpgAssuanHelper::GpgAssuanHelper(int channel) + : GpgFrontend::SingletonFunctionObject<GpgAssuanHelper>(channel), + gpgconf_path_(Module::RetrieveRTValueTypedOrDefault<>( + "core", "gpgme.ctx.gpgconf_path", QString{})) {} + +GpgAssuanHelper::~GpgAssuanHelper() { + for (const auto& ctx : assuan_ctx_) { + assuan_release(ctx); + } +} + +auto GpgAssuanHelper::ConnectToSocket(GpgComponentType type) -> bool { + if (assuan_ctx_.contains(type)) return true; + + auto socket_path = ctx_.ComponentDirectory(type); + if (socket_path.isEmpty()) { + LOG_F() << "socket path of component: " << component_type_to_q_string(type) + << " is empty"; + return false; + } + + QFileInfo info(socket_path); + if (!info.exists()) { + LOG_W() << "socket path is not exists: " << socket_path; + + LOG_W() << "launching component: " << component_type_to_q_string(type) + << " by gpgconf, sockets: " << socket_path; + launch_component(type); + + if (!info.exists()) { + LOG_F() << "socket path is still not exists: " << socket_path + << "abort..."; + return false; + } + } + + assuan_context_t a_ctx; + assuan_new(&a_ctx); + + 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); + return false; + } + + LOG_D() << "connected to socket by assuan protocol: " + << info.absoluteFilePath(); + + 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); + return false; + } + + assuan_ctx_[type] = a_ctx; + return true; +} + +auto GpgAssuanHelper::SendCommand(GpgComponentType type, const QString& command, + DataCallback data_cb, + InqueryCallback inquery_cb, + StatusCallback status_cb) -> bool { + if (!assuan_ctx_.contains(type)) { + LOG_W() << "haven't connect to: " << component_type_to_q_string(type) + << ", trying to make a connection"; + if (!ConnectToSocket(type)) return false; + } + + auto context = QSharedPointer<AssuanCallbackContext>::create(); + context->self = this; + context->data_cb = std::move(data_cb); + context->status_cb = std::move(status_cb); + context->inquery_cb = std::move(inquery_cb); + + 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); + return false; + } + + return true; +} + +auto GpgAssuanHelper::SendStatusCommand(GpgComponentType type, + const QString& command) + -> std::tuple<bool, QStringList> { + GpgAssuanHelper::DataCallback d_cb = + [&](const QSharedPointer<GpgAssuanHelper::AssuanCallbackContext>& ctx) + -> gpg_error_t { + LOG_D() << "data callback of command " << command << ": " << ctx->buffer; + + return 0; + }; + + GpgAssuanHelper::InqueryCallback i_cb = + [=](const QSharedPointer<GpgAssuanHelper::AssuanCallbackContext>& ctx) + -> gpg_error_t { + LOG_D() << "inquery callback of command: " << command << ": " + << ctx->inquery; + return 0; + }; + + QStringList status_lines; + GpgAssuanHelper::StatusCallback s_cb = + [&](const QSharedPointer<GpgAssuanHelper::AssuanCallbackContext>& ctx) + -> gpg_error_t { + LOG_D() << "status callback of command: " << command << ": " + << ctx->status; + status_lines.append(ctx->status); + return 0; + }; + + auto ret = SendCommand(type, command, d_cb, i_cb, s_cb); + + return {ret, status_lines}; +} + +auto GpgAssuanHelper::default_data_callback(void* opaque, const void* buffer, + size_t length) -> gpgme_error_t { + auto ctx = *static_cast<QSharedPointer<AssuanCallbackContext>*>(opaque); + ctx->buffer.clear(); + ctx->buffer.append(static_cast<const char*>(buffer), + static_cast<qsizetype>(length)); + if (ctx->data_cb) ctx->data_cb(ctx); + return GPG_ERR_NO_ERROR; +} + +auto GpgAssuanHelper::default_status_callback(void* opaque, const char* status) + -> gpgme_error_t { + auto ctx = *static_cast<QSharedPointer<AssuanCallbackContext>*>(opaque); + ctx->status = QString::fromUtf8(status); + if (ctx->status_cb) ctx->status_cb(ctx); + return GPG_ERR_NO_ERROR; +} + +auto GpgAssuanHelper::default_inquery_callback( + void* opaque, const char* inquery) -> gpgme_error_t { + auto ctx = *static_cast<QSharedPointer<AssuanCallbackContext>*>(opaque); + ctx->inquery = QString::fromUtf8(inquery); + if (ctx->status_cb) ctx->inquery_cb(ctx); + return GPG_ERR_NO_ERROR; +} + +void GpgAssuanHelper::launch_component(GpgComponentType type) { + if (gpgconf_path_.isEmpty()) { + LOG_F() << "gpgconf_path is not collected by initializing"; + return; + } + + auto gpgconf_path = QFileInfo(gpgconf_path_).absoluteFilePath(); + LOG_D() << "assuan helper channel: " << GetChannel() + << "gpgconf path: " << gpgconf_path; + + QProcess process; + process.setProgram(gpgconf_path); + process.setArguments({"--launch", component_type_to_q_string(type)}); + process.start(); + + if (!process.waitForFinished()) { + LOG_F() << "failed to execute gpgconf" << process.arguments(); + return; + } +} + +auto GpgAssuanHelper::component_type_to_q_string(GpgComponentType type) + -> QString { + switch (type) { + case GpgComponentType::kGPG_AGENT: + case GpgComponentType::kGPG_AGENT_SSH: + return "gpg-agent"; + case GpgComponentType::kDIRMNGR: + return "dirmngr"; + case GpgComponentType::kKEYBOXD: + return "keyboxd"; + default: + return "all"; + } +} +auto GpgAssuanHelper::simple_data_callback(void* opaque, const void* buffer, + size_t length) -> gpgme_error_t { + LOG_D() << "assuan callback data: " + << QByteArray::fromRawData(static_cast<const char*>(buffer), length); + return 0; +} + +auto GpgAssuanHelper::AssuanCallbackContext::SendData(const QByteArray& b) const + -> gpg_error_t { + return assuan_send_data(ctx, b.constData(), b.size()); +} +} // namespace GpgFrontend
\ No newline at end of file diff --git a/src/core/function/gpg/GpgAssuanHelper.h b/src/core/function/gpg/GpgAssuanHelper.h new file mode 100644 index 00000000..65ec325f --- /dev/null +++ b/src/core/function/gpg/GpgAssuanHelper.h @@ -0,0 +1,103 @@ +/** + * Copyright (C) 2021-2024 Saturneric <[email protected]> + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see <https://www.gnu.org/licenses/>. + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric <[email protected]> starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include <assuan.h> + +#include "core/function/gpg/GpgContext.h" + +namespace GpgFrontend { + +class GPGFRONTEND_CORE_EXPORT GpgAssuanHelper + : public SingletonFunctionObject<GpgAssuanHelper> { + public: + struct AssuanCallbackContext; + + using DataCallback = + std::function<gpg_error_t(QSharedPointer<AssuanCallbackContext>)>; + using InqueryCallback = + std::function<gpg_error_t(QSharedPointer<AssuanCallbackContext>)>; + using StatusCallback = + std::function<gpg_error_t(QSharedPointer<AssuanCallbackContext>)>; + + struct AssuanCallbackContext { + GpgAssuanHelper* self; + GpgComponentType component_type; + assuan_context_t ctx; + + QByteArray buffer; + QString status; + QString inquery; + + DataCallback data_cb; + InqueryCallback inquery_cb; + StatusCallback status_cb; + + [[nodiscard]] auto SendData(const QByteArray& b) const -> gpg_error_t; + }; + + explicit GpgAssuanHelper(int channel); + ~GpgAssuanHelper(); + + auto ConnectToSocket(GpgComponentType) -> bool; + + auto SendCommand(GpgComponentType type, const QString& command, + DataCallback data_cb, InqueryCallback inquery_cb, + StatusCallback status_cb) -> bool; + + auto SendStatusCommand(GpgComponentType type, const QString& command) + -> std::tuple<bool, QStringList>; + + private: + GpgContext& ctx_ = + GpgContext::GetInstance(SingletonFunctionObject::GetChannel()); + QMap<GpgComponentType, assuan_context_t> assuan_ctx_; + + void launch_component(GpgComponentType type); + + static auto component_type_to_q_string(GpgComponentType type) -> QString; + + static auto simple_data_callback(void* opaque, const void* buffer, + size_t length) -> gpgme_error_t; + + static auto default_data_callback(void* opaque, const void* buffer, + size_t length) -> gpgme_error_t; + + static auto default_status_callback(void* opaque, + const char* status) -> gpgme_error_t; + + static auto default_inquery_callback(void* opaque, + const char* inquery) -> gpgme_error_t; + + QByteArray temp_data_; + QString temp_status_; + QString gpgconf_path_; +}; + +}; // namespace GpgFrontend
\ No newline at end of file diff --git a/src/core/function/gpg/GpgAutomatonHandler.h b/src/core/function/gpg/GpgAutomatonHandler.h index 075edb82..78b20252 100644 --- a/src/core/function/gpg/GpgAutomatonHandler.h +++ b/src/core/function/gpg/GpgAutomatonHandler.h @@ -1,5 +1,3 @@ - - /** * Copyright (C) 2021-2024 Saturneric <[email protected]> * diff --git a/src/core/function/gpg/GpgContext.cpp b/src/core/function/gpg/GpgContext.cpp index e93544ba..ce84ed2e 100644 --- a/src/core/function/gpg/GpgContext.cpp +++ b/src/core/function/gpg/GpgContext.cpp @@ -52,13 +52,15 @@ namespace GpgFrontend { class GpgContext::Impl { public: /** - * Constructor - * Set up gpgme-context, set paths to app-run path + * @brief Construct a new Impl object + * + * @param parent + * @param args */ Impl(GpgContext *parent, const GpgContextInitArgs &args) - : parent_(parent), - args_(args), - good_(default_ctx_initialize(args) && binary_ctx_initialize(args)) {} + : parent_(parent), args_(args) { + init(args); + } ~Impl() { if (ctx_ref_ != nullptr) { @@ -180,15 +182,47 @@ class GpgContext::Impl { return GPG_ERR_NO_ERROR; } + [[nodiscard]] auto HomeDirectory() const -> QString { return database_path_; } + + [[nodiscard]] auto ComponentDirectories(GpgComponentType type) const + -> QString { + return component_dirs_.value(component_type_to_q_string(type), ""); + } + private: GpgContext *parent_; GpgContextInitArgs args_{}; ///< gpgme_ctx_t ctx_ref_ = nullptr; ///< gpgme_ctx_t binary_ctx_ref_ = nullptr; ///< bool good_ = true; + std::mutex ctx_ref_lock_; std::mutex binary_ctx_ref_lock_; + QString gpgconf_path_; + QString database_path_; + QMap<QString, QString> component_dirs_; + + void init(const GpgContextInitArgs &args) { + good_ = default_ctx_initialize(args) && binary_ctx_initialize(args); + get_gpg_conf_dirs(); + } + + static auto component_type_to_q_string(GpgComponentType type) -> QString { + switch (type) { + case GpgComponentType::kGPG_AGENT: + return "agent-socket"; + case GpgComponentType::kGPG_AGENT_SSH: + return "agent-ssh-socket"; + case GpgComponentType::kDIRMNGR: + return "dirmngr-socket"; + case GpgComponentType::kKEYBOXD: + return "keyboxd-socket"; + default: + return ""; + } + } + static auto set_ctx_key_list_mode(const gpgme_ctx_t &ctx) -> bool { assert(ctx != nullptr); @@ -213,10 +247,9 @@ class GpgContext::Impl { const auto app_path = Module::RetrieveRTValueTypedOrDefault<>( "core", QString("gpgme.ctx.app_path"), QString{}); - QString database_path; // set custom gpg key db path if (!args_.db_path.isEmpty()) { - database_path = args_.db_path; + database_path_ = args_.db_path; } LOG_D() << "ctx set engine info, channel: " << parent_->GetChannel() @@ -224,12 +257,12 @@ class GpgContext::Impl { << ", app path: " << app_path; auto app_path_buffer = app_path.toUtf8(); - auto database_path_buffer = database_path.toUtf8(); + auto database_path_buffer = database_path_.toUtf8(); auto err = gpgme_ctx_set_engine_info( ctx, gpgme_get_protocol(ctx), app_path.isEmpty() ? nullptr : app_path_buffer, - database_path.isEmpty() ? nullptr : database_path_buffer); + database_path_.isEmpty() ? nullptr : database_path_buffer); assert(CheckGpgError(err) == GPG_ERR_NO_ERROR); return CheckGpgError(err) == GPG_ERR_NO_ERROR; @@ -241,10 +274,14 @@ class GpgContext::Impl { const GpgContextInitArgs &args) -> bool { assert(ctx != nullptr); - if (!args.gpgconf_path.isEmpty()) { - LOG_D() << "set gpgconf path: " << args.gpgconf_path; + this->gpgconf_path_ = Module::RetrieveRTValueTypedOrDefault<>( + "core", "gpgme.ctx.gpgconf_path", QString{}); + assert(!gpgconf_path_.isEmpty()); + + if (!gpgconf_path_.isEmpty()) { + LOG_D() << "set gpgconf path: " << gpgconf_path_; auto err = gpgme_ctx_set_engine_info(ctx, GPGME_PROTOCOL_GPGCONF, - args.gpgconf_path.toUtf8(), nullptr); + gpgconf_path_.toUtf8(), nullptr); if (CheckGpgError(err) != GPG_ERR_NO_ERROR) { LOG_W() << "set gpg context engine info error: " @@ -341,6 +378,45 @@ class GpgContext::Impl { gpgme_set_armor(ctx_ref_, 1); return true; } + + void get_gpg_conf_dirs() { + auto gpgconf_path = QFileInfo(this->gpgconf_path_).absoluteFilePath(); + LOG_D() << "context: " << parent_->GetChannel() + << "gpgconf path: " << gpgconf_path; + + QProcess process; + process.setProgram(gpgconf_path); + process.setArguments({"--list-dirs"}); + process.start(); + + if (!process.waitForFinished()) { + LOG_F() << "failed to execute gpgconf --list-dirs"; + return; + } + + const QString output = QString::fromUtf8(process.readAllStandardOutput()); + const QStringList lines = output.split('\n', Qt::SkipEmptyParts); + + for (const QString &line : lines) { + auto info_split_list = line.split(":"); + if (info_split_list.size() != 2) continue; + + auto configuration_name = info_split_list[0].trimmed(); + auto configuration_value = info_split_list[1].trimmed(); + +#ifdef __MINGW32__ + // replace some special substrings on windows + // platform + configuration_value.replace("%3a", ":"); +#endif + + LOG_D() << "channel: " << parent_->GetChannel() + << "component: " << configuration_name + << "dir:" << configuration_value; + + component_dirs_[configuration_name] = configuration_value; + } + } }; GpgContext::GpgContext(int channel) @@ -361,4 +437,12 @@ auto GpgContext::DefaultContext() -> gpgme_ctx_t { GpgContext::~GpgContext() = default; +auto GpgContext::HomeDirectory() const -> QString { + return p_->HomeDirectory(); +} + +auto GpgContext::ComponentDirectory(GpgComponentType type) const -> QString { + return p_->ComponentDirectories(type); +} + } // namespace GpgFrontend
\ No newline at end of file diff --git a/src/core/function/gpg/GpgContext.h b/src/core/function/gpg/GpgContext.h index f98adde8..4c683c0b 100644 --- a/src/core/function/gpg/GpgContext.h +++ b/src/core/function/gpg/GpgContext.h @@ -47,11 +47,11 @@ struct GpgContextInitArgs { bool offline_mode = false; ///< bool auto_import_missing_key = false; ///< - QString gpgconf_path; ///< - bool use_pinentry = false; ///< }; +enum class GpgComponentType { kGPG_AGENT, kDIRMNGR, kKEYBOXD, kGPG_AGENT_SSH }; + /** * @brief * @@ -71,6 +71,10 @@ class GPGFRONTEND_CORE_EXPORT GpgContext auto DefaultContext() -> gpgme_ctx_t; + [[nodiscard]] auto HomeDirectory() const -> QString; + + [[nodiscard]] auto ComponentDirectory(GpgComponentType) const -> QString; + private: class Impl; SecureUniquePtr<Impl> p_; diff --git a/src/test/core/GpgCoreTestAssuan.cpp b/src/test/core/GpgCoreTestAssuan.cpp new file mode 100644 index 00000000..cc61b27c --- /dev/null +++ b/src/test/core/GpgCoreTestAssuan.cpp @@ -0,0 +1,76 @@ +/** + * 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 "GpgCoreTest.h" +#include "core/function/gpg/GpgAssuanHelper.h" + +namespace GpgFrontend::Test { + +TEST_F(GpgCoreTest, CoreAssuanConnectTestA) { + auto& helper = GpgAssuanHelper::GetInstance(); + + auto ret = helper.ConnectToSocket(GpgComponentType::kGPG_AGENT); + ASSERT_TRUE(ret); + + GpgAssuanHelper::DataCallback d_cb = + [=](const QSharedPointer<GpgAssuanHelper::AssuanCallbackContext>& ctx) + -> gpg_error_t { + LOG_D() << "data callback of command GETINFO pid: " << ctx->buffer; + return 0; + }; + + GpgAssuanHelper::InqueryCallback i_cb = + [](const QSharedPointer<GpgAssuanHelper::AssuanCallbackContext>& ctx) + -> gpg_error_t { + LOG_D() << "inquery callback of command GETINFO pid: " << ctx->inquery; + return 0; + }; + + GpgAssuanHelper::StatusCallback s_cb = + [](const QSharedPointer<GpgAssuanHelper::AssuanCallbackContext>& ctx) + -> gpg_error_t { + LOG_D() << "status callback of command GETINFO pid: " << ctx->status; + return 0; + }; + + ret = helper.SendCommand(GpgComponentType::kGPG_AGENT, "GETINFO pid", d_cb, + i_cb, s_cb); + ASSERT_TRUE(ret); +} + +TEST_F(GpgCoreTest, CoreAssuanConnectTestB) { + auto& helper = GpgAssuanHelper::GetInstance(); + + auto [ret, status] = + helper.SendStatusCommand(GpgComponentType::kGPG_AGENT, "keyinfo --list"); + ASSERT_TRUE(ret); + ASSERT_TRUE(!status.isEmpty()); + + LOG_D() << "status lines of command keyinfo --list: " << status; +} +} // namespace GpgFrontend::Test
\ No newline at end of file |