diff options
Diffstat (limited to 'src')
50 files changed, 7276 insertions, 101 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 88ec8148..31b865d6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,7 +44,7 @@ find_package(Gpgme REQUIRED) find_package(Config++ REQUIRED) # Introduce OpenSSL -if(APPLE) +if (APPLE) set(OPENSSL_ROOT_DIR /usr/local/opt/openssl@3) endif() find_package(OpenSSL REQUIRED) @@ -86,6 +86,10 @@ if (BUILD_APPLICATION) endif () if (BUILD_CORE) + # core depends pinentry + message("[+] Build Pinentry") + add_subdirectory(pinentry) + message("[+] Build Core") add_subdirectory(core) endif () diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index bb8cdf77..137acc7b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -24,8 +24,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later -aux_source_directory(./function/basic CORE_SOURCE) aux_source_directory(./function/result_analyse CORE_SOURCE) +aux_source_directory(./function/basic CORE_SOURCE) aux_source_directory(./function/gpg CORE_SOURCE) aux_source_directory(./function CORE_SOURCE) aux_source_directory(./thread CORE_SOURCE) @@ -43,6 +43,9 @@ add_library(gpgfrontend_core SHARED ${CORE_SOURCE}) set(_export_file "${CMAKE_CURRENT_SOURCE_DIR}/GpgFrontendCoreExport.h") generate_export_header(gpgfrontend_core EXPORT_FILE_NAME "${_export_file}") +# link buddled pinentry +target_link_libraries(gpgfrontend_core PRIVATE gpgfrontend_pinentry) + # link third-party libraries target_link_libraries(gpgfrontend_core PUBLIC config++) if (NOT LINUX) diff --git a/src/core/function/CoreSignalStation.h b/src/core/function/CoreSignalStation.h index 35177a7f..a53f2eb4 100644 --- a/src/core/function/CoreSignalStation.h +++ b/src/core/function/CoreSignalStation.h @@ -54,13 +54,13 @@ class GPGFRONTEND_CORE_EXPORT CoreSignalStation : public QObject { * @brief * */ - void SignalUserInputPassphraseDone(); + void SignalNeedUserInputPassphrase(); /** * @brief * */ - void SignalNeedUserInputPassphrase(); + void SignalUserInputPassphraseCallback(QByteArray); /** * @brief diff --git a/src/core/function/gpg/GpgContext.cpp b/src/core/function/gpg/GpgContext.cpp index e6975ea4..9ee81bad 100644 --- a/src/core/function/gpg/GpgContext.cpp +++ b/src/core/function/gpg/GpgContext.cpp @@ -30,10 +30,19 @@ #include <gpg-error.h> #include <gpgme.h> +#include <qapplication.h> +#include <qcoreapplication.h> +#include <qeasingcurve.h> #include <qeventloop.h> +#include <qlabel.h> #include <qobject.h> +#include <qtmetamacros.h> +#include <qwindowdefs.h> +#include <sys/types.h> #include <unistd.h> +#include <cstring> + #include "core/function/CoreSignalStation.h" #include "core/function/basic/GpgFunctionObject.h" #include "core/function/gpg/GpgCommandExecutor.h" @@ -44,6 +53,7 @@ #include "core/utils/CacheUtils.h" #include "core/utils/CommonUtils.h" #include "core/utils/GpgUtils.h" +#include "function/CacheManager.h" #include "spdlog/spdlog.h" #ifdef _WIN32 @@ -247,36 +257,28 @@ class GpgContext::Impl : public SingletonFunctionObject<GpgContext::Impl> { const char *passphrase_info, int last_was_bad, int fd) -> gpgme_error_t { auto *p_ctx = static_cast<GpgContext *>(hook); - SPDLOG_DEBUG("custom passphrase cb called, bad times: {}", last_was_bad); - - if (last_was_bad > 3) { - SPDLOG_WARN("failure_counts is over three times"); - return gpgme_error_from_errno(GPG_ERR_CANCELED); - } + std::string passphrase; - std::string passphrase = GetTempCacheValue("__key_passphrase"); - // no pawword is an error situation - if (passphrase.empty()) { - // user input passphrase - SPDLOG_DEBUG("might need user to input passparase"); + SPDLOG_DEBUG( + "custom passphrase cb called, uid: {}, info: {}, last_was_bad: {}", + uid_hint == nullptr ? "<empty>" : std::string{uid_hint}, + passphrase_info == nullptr ? "<empty>" : std::string{passphrase_info}, + last_was_bad); - p_ctx->ShowPasswordInputDialog(); - passphrase = GetTempCacheValue("__key_passphrase"); + emit CoreSignalStation::GetInstance()->SignalNeedUserInputPassphrase(); - SPDLOG_DEBUG("use may has inputed the passphrase"); - - if (passphrase.empty()) { - SPDLOG_ERROR("cannot get passphrase from use or passphrase is empty"); - - gpgme_io_write(fd, "\n", 1); - return gpgme_error_from_errno(GPG_ERR_CANCELED); - } - } + QEventLoop looper; + QObject::connect( + CoreSignalStation::GetInstance(), + &CoreSignalStation::SignalUserInputPassphraseCallback, &looper, + [&](const QByteArray &buffer) { passphrase = buffer.toStdString(); }); + QObject::connect(CoreSignalStation::GetInstance(), + &CoreSignalStation::SignalUserInputPassphraseCallback, + &looper, &QEventLoop::quit); + looper.exec(); - // the user must at least write a newline character before returning from - // the callback. - passphrase = passphrase.append("\n"); auto passpahrase_size = passphrase.size(); + SPDLOG_DEBUG("get passphrase from pinentry size: {}", passpahrase_size); size_t off = 0; size_t res = 0; @@ -285,8 +287,12 @@ class GpgContext::Impl : public SingletonFunctionObject<GpgContext::Impl> { if (res > 0) off += res; } while (res > 0 && off != passpahrase_size); - return off == passpahrase_size ? 0 - : gpgme_error_from_errno(GPG_ERR_CANCELED); + res += gpgme_io_write(fd, "\n", 1); + + SPDLOG_DEBUG("custom passphrase cd is about to return, res: {}", res); + return res == passpahrase_size + 1 + ? 0 + : gpgme_error_from_errno(GPG_ERR_CANCELED); } static auto TestStatusCb(void *hook, const char *keyword, const char *args) @@ -295,18 +301,6 @@ class GpgContext::Impl : public SingletonFunctionObject<GpgContext::Impl> { return GPG_ERR_NO_ERROR; } - void ShowPasswordInputDialog() { - emit parent_->SignalNeedUserInputPassphrase(); - - QEventLoop looper; - QObject::connect(CoreSignalStation::GetInstance(), - &CoreSignalStation::SignalUserInputPassphraseDone, &looper, - &QEventLoop::quit); - looper.exec(); - - SPDLOG_DEBUG("show password input dialog done"); - } - private: struct CtxRefDeleter { void operator()(gpgme_ctx_t _ctx) { @@ -364,10 +358,6 @@ class GpgContext::Impl : public SingletonFunctionObject<GpgContext::Impl> { SPDLOG_DEBUG("ctx set custom key db path: {}", database_path); assert(CheckGpgError(err) == GPG_ERR_NO_ERROR); } - - QObject::connect(parent_, &GpgContext::SignalNeedUserInputPassphrase, - CoreSignalStation::GetInstance(), - &CoreSignalStation::SignalNeedUserInputPassphrase); } }; @@ -389,10 +379,6 @@ GpgContext::operator gpgme_ctx_t() const { return static_cast<gpgme_ctx_t>(*p_); } -void GpgContext::ShowPasswordInputDialog() { - return p_->ShowPasswordInputDialog(); -} - GpgContext::~GpgContext() = default; } // 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 b9042128..527099fc 100644 --- a/src/core/function/gpg/GpgContext.h +++ b/src/core/function/gpg/GpgContext.h @@ -71,11 +71,6 @@ class GPGFRONTEND_CORE_EXPORT GpgContext void SetPassphraseCb(gpgme_passphrase_cb_t passphrase_cb) const; - void ShowPasswordInputDialog(); - - signals: - void SignalNeedUserInputPassphrase(); - private: class Impl; std::unique_ptr<Impl> p_; diff --git a/src/core/function/gpg/GpgKeyOpera.cpp b/src/core/function/gpg/GpgKeyOpera.cpp index e48a84d8..64f33373 100644 --- a/src/core/function/gpg/GpgKeyOpera.cpp +++ b/src/core/function/gpg/GpgKeyOpera.cpp @@ -28,6 +28,9 @@ #include "GpgKeyOpera.h" +#include <gpg-error.h> +#include <qeventloop.h> + #include <boost/algorithm/string.hpp> #include <boost/asio.hpp> #include <boost/date_time/posix_time/conversion.hpp> @@ -41,8 +44,12 @@ #include "core/function/result_analyse/GpgResultAnalyse.h" #include "core/model/GpgGenKeyInfo.h" #include "core/module/ModuleManager.h" +#include "core/thread/Task.h" +#include "core/thread/TaskRunnerGetter.h" #include "core/utils/CommonUtils.h" #include "core/utils/GpgUtils.h" +#include "model/DataObject.h" +#include "spdlog/spdlog.h" namespace GpgFrontend { @@ -271,17 +278,33 @@ auto GpgKeyOpera::GenerateSubkey(const GpgKey& key, return CheckGpgError(err); } -auto GpgKeyOpera::ModifyPassword(const GpgKey& key) -> GpgError { +void GpgKeyOpera::ModifyPassword(const GpgKey& key, + std::function<void(gpgme_error_t)> callback) { const auto gnupg_version = Module::RetrieveRTValueTypedOrDefault<>( "core", "gpgme.ctx.gnupg_version", std::string{"2.0.0"}); SPDLOG_DEBUG("got gnupg version from rt: {}", gnupg_version); if (CompareSoftwareVersion(gnupg_version, "2.0.15") < 0) { SPDLOG_ERROR("operator not support"); - return GPG_ERR_NOT_SUPPORTED; + callback(GPG_ERR_NOT_SUPPORTED); + return; } - auto err = gpgme_op_passwd(ctx_, static_cast<gpgme_key_t>(key), 0); - return CheckGpgError(err); + + auto *task = new Thread::Task( + [&](const DataObjectPtr& data_object) -> int { + auto err = gpgme_op_passwd(ctx_, static_cast<gpgme_key_t>(key), 0); + data_object->Swap({err}); + return 0; + }, + "gpgme_op_passwd", TransferParams(), + [=](int, const DataObjectPtr& data_object) { + SPDLOG_DEBUG("callback called"); + callback(ExtractParams<gpgme_error_t>(data_object, 0)); + }); + + Thread::TaskRunnerGetter::GetInstance() + .GetTaskRunner(Thread::TaskRunnerGetter::kTaskRunnerType_GPG) + ->PostTask(task); } auto GpgKeyOpera::ModifyTOFUPolicy(const GpgKey& key, diff --git a/src/core/function/gpg/GpgKeyOpera.h b/src/core/function/gpg/GpgKeyOpera.h index 0186cdbb..dd2ee60d 100644 --- a/src/core/function/gpg/GpgKeyOpera.h +++ b/src/core/function/gpg/GpgKeyOpera.h @@ -28,6 +28,8 @@ #pragma once +#include <functional> + #include "core/function/gpg/GpgContext.h" #include "core/function/result_analyse/GpgResultAnalyse.h" #include "core/typedef/GpgTypedef.h" @@ -98,7 +100,7 @@ class GPGFRONTEND_CORE_EXPORT GpgKeyOpera * @param key * @return GpgFrontend::GpgError */ - auto ModifyPassword(const GpgKey& key) -> GpgFrontend::GpgError; + void ModifyPassword(const GpgKey& key, std::function<void(gpgme_error_t)>); /** * @brief diff --git a/src/core/thread/Task.cpp b/src/core/thread/Task.cpp index d7f1dab9..92592bf4 100644 --- a/src/core/thread/Task.cpp +++ b/src/core/thread/Task.cpp @@ -89,7 +89,7 @@ class Task::Impl : public QObject { std::string GetUUID() const { return uuid_; } void Run() { - SPDLOG_DEBUG("task {} is using default runnable and callback mode", + SPDLOG_TRACE("task {} is using default runnable and callback mode", GetFullID()); if (runnable_) { SetRTN(runnable_(data_object_)); @@ -184,27 +184,27 @@ class Task::Impl : public QObject { try { if (callback_) { - SPDLOG_DEBUG("task {} has a callback function", GetFullID()); + SPDLOG_TRACE("task {} has a callback function", GetFullID()); if (callback_thread_ == QThread::currentThread()) { - SPDLOG_DEBUG("for task {}, the callback thread is the same thread", + SPDLOG_TRACE("for task {}, the callback thread is the same thread", GetFullID(), callback_thread_->currentThreadId()); callback_(rtn, data_object_); // raise signal, announcing this task comes to an end - SPDLOG_DEBUG( + SPDLOG_TRACE( "for task {}, its life comes to an end in the same thread after " "its callback executed.", parent_->GetFullID()); emit parent_->SignalTaskEnd(); } else { - SPDLOG_DEBUG("for task {}, callback thread is a different thread: {}", + SPDLOG_TRACE("for task {}, callback thread is a different thread: {}", GetFullID(), callback_thread_->currentThreadId()); if (!QMetaObject::invokeMethod( callback_thread_, [callback = callback_, rtn = rtn_, data_object = data_object_, parent_ = this->parent_]() { - SPDLOG_DEBUG("calling callback of task {}", + SPDLOG_TRACE("calling callback of task {}", parent_->GetFullID()); try { callback(rtn, data_object); @@ -215,7 +215,7 @@ class Task::Impl : public QObject { parent_->GetFullID()); } // raise signal, announcing this task comes to an end - SPDLOG_DEBUG( + SPDLOG_TRACE( "for task {}, its life comes to an end whether its " "callback function fails or not.", parent_->GetFullID()); @@ -225,7 +225,7 @@ class Task::Impl : public QObject { "task {} had failed to invoke the callback function to target " "thread", GetFullID()); - SPDLOG_DEBUG( + SPDLOG_TRACE( "for task {}, its life must come to an end now, although it " "has something not done yet.", GetFullID()); @@ -234,7 +234,7 @@ class Task::Impl : public QObject { } } else { // raise signal, announcing this task comes to an end - SPDLOG_DEBUG( + SPDLOG_TRACE( "for task {}, its life comes to an end without callback " "peacefully.", GetFullID()); @@ -246,7 +246,7 @@ class Task::Impl : public QObject { "stacktrace of the exception: {}", boost::stacktrace::to_string(boost::stacktrace::stacktrace())); // raise signal, announcing this task comes to an end - SPDLOG_DEBUG("for task {}, its life comes to an end at chaos.", + SPDLOG_TRACE("for task {}, its life comes to an end at chaos.", GetFullID()); emit parent_->SignalTaskEnd(); } catch (...) { @@ -255,7 +255,7 @@ class Task::Impl : public QObject { "stacktrace of the exception: {}", boost::stacktrace::to_string(boost::stacktrace::stacktrace())); // raise signal, announcing this task comes to an end - SPDLOG_DEBUG("for task {}, its life comes to an end at unknown chaos.", + SPDLOG_TRACE("for task {}, its life comes to an end at unknown chaos.", GetFullID()); emit parent_->SignalTaskEnd(); } @@ -291,7 +291,7 @@ void Task::SafelyRun() { emit SignalRun(); } void Task::Run() { p_->Run(); } void Task::run() { - SPDLOG_DEBUG("interface run() of task {} was called by thread: {}", + SPDLOG_TRACE("interface run() of task {} was called by thread: {}", GetFullID(), QThread::currentThread()->currentThreadId()); this->SafelyRun(); } diff --git a/src/pinentry/CMakeLists.txt b/src/pinentry/CMakeLists.txt new file mode 100644 index 00000000..e6d53feb --- /dev/null +++ b/src/pinentry/CMakeLists.txt @@ -0,0 +1,69 @@ +# Copyright (C) 2021 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 + +aux_source_directory(. PINENTRY_SOURCE) + +# capslock +list(APPEND PINENTRY_SOURCE "capslock/capslock.cpp") +if (MINGW) + list(APPEND PINENTRY_SOURCE "capslock/capslock_win.cpp") +else() + list(APPEND PINENTRY_SOURCE "capslock/capslock_unix.cpp") +endif() + +message(STATUS "PINENTRY_SOURCE: ${PINENTRY_SOURCE}") +add_library(gpgfrontend_pinentry SHARED ${PINENTRY_SOURCE}) + +# link Qt core +if(Qt6_DIR) + target_link_libraries(gpgfrontend_pinentry PUBLIC Qt6::Core Qt6::Widgets) +else() + target_link_libraries(gpgfrontend_pinentry PUBLIC Qt5::Core Qt5::Widgets) +endif() + +# using std c++ 17 +target_compile_features(gpgfrontend_pinentry PUBLIC cxx_std_17) + +# link for different platforms +if (MINGW) + message(STATUS "Link GPG Static Library For MINGW") + target_link_libraries(gpgfrontend_pinentry PUBLIC wsock32) +elseif (APPLE) + message(STATUS "Link GPG Static Library For macOS") + target_link_libraries(gpgfrontend_pinentry PUBLIC dl) + if (XCODE_BUILD) + set_target_properties(gpgfrontend_pinentry + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE} + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE} + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE} + XCODE_ATTRIBUTE_SKIP_INSTALL "Yes" + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "${GPGFRONTEND_XOCDE_CODE_SIGN_IDENTITY}") + endif () +else () + # linux + message(STATUS "Link GPG Static Library For Unix") + target_link_libraries(gpgfrontend_pinentry PUBLIC pthread dl) +endif () diff --git a/src/pinentry/accessibility.cpp b/src/pinentry/accessibility.cpp new file mode 100644 index 00000000..f139832a --- /dev/null +++ b/src/pinentry/accessibility.cpp @@ -0,0 +1,44 @@ +/* accessibility.cpp - Helpers for making pinentry accessible + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "accessibility.h" + +#include <QString> +#include <QWidget> + +namespace Accessibility { + +void setDescription(QWidget *w, const QString &text) { + if (w) { +#ifndef QT_NO_ACCESSIBILITY + w->setAccessibleDescription(text); +#endif + } +} + +void setName(QWidget *w, const QString &text) { + if (w) { +#ifndef QT_NO_ACCESSIBILITY + w->setAccessibleName(text); +#endif + } +} + +} // namespace Accessibility diff --git a/src/pinentry/accessibility.h b/src/pinentry/accessibility.h new file mode 100644 index 00000000..9ef912d6 --- /dev/null +++ b/src/pinentry/accessibility.h @@ -0,0 +1,40 @@ +/* accessibility.h - Helpers for making pinentry accessible + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_ACCESSIBILITY_H__ +#define __PINENTRY_QT_ACCESSIBILITY_H__ + +class QString; +class QWidget; + +namespace Accessibility +{ + +/* Wrapper for QWidget::setAccessibleDescription which does nothing if + QT_NO_ACCESSIBILITY is defined. */ +void setDescription(QWidget *w, const QString &text); + +/* Wrapper for QWidget::setAccessibleName which does nothing if + QT_NO_ACCESSIBILITY is defined. */ +void setName(QWidget *w, const QString &text); + +} // namespace Accessibility + +#endif // __PINENTRY_QT_ACCESSIBILITY_H__ diff --git a/src/pinentry/argparse.cpp b/src/pinentry/argparse.cpp new file mode 100644 index 00000000..d3568c64 --- /dev/null +++ b/src/pinentry/argparse.cpp @@ -0,0 +1,1360 @@ +/* [argparse.c wk 17.06.97] Argument Parser for option handling + * Copyright (C) 1998-2001, 2006-2008, 2012 Free Software Foundation, Inc. + * Copyright (C) 1997-2001, 2006-2008, 2013-2015 Werner Koch + * + * This file is part of JNLIB, which is a subsystem of GnuPG. + * + * JNLIB is free software; you can redistribute it and/or modify it + * under the terms of either + * + * - the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at + * your option) any later version. + * + * or + * + * - the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * or both in parallel, as here. + * + * JNLIB 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 copies of the GNU General Public License + * and the GNU Lesser General Public License along with this program; + * if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: (GPL-2.0+ OR LGPL-3.0+) + */ + +/* This file may be used as part of GnuPG or standalone. A GnuPG + build is detected by the presence of the macro GNUPG_MAJOR_VERSION. + Some feature are only availalbe in the GnuPG build mode. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <ctype.h> +#include <errno.h> +#include <limits.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifdef GNUPG_MAJOR_VERSION +#include "libjnlib-config.h" +#include "logging.h" +#include "mischelp.h" +#include "stringhelp.h" +#ifdef JNLIB_NEED_UTF8CONV +#include "utf8conv.h" +#endif +#endif /*GNUPG_MAJOR_VERSION*/ + +#include "argparse.h" + +/* GnuPG uses GPLv3+ but a standalone version of this defaults to + GPLv2+ because that is the license of this file. Change this if + you include it in a program which uses GPLv3. If you don't want to + set a a copyright string for your usage() you may also hardcode it + here. */ +#ifndef GNUPG_MAJOR_VERSION + +#define ARGPARSE_GPL_VERSION 2 +#define ARGPARSE_CRIGHT_STR "Copyright (C) YEAR NAME" + +#else /* Used by GnuPG */ + +#define ARGPARSE_GPL_VERSION 3 +#define ARGPARSE_CRIGHT_STR "Copyright (C) 2015 Free Software Foundation, Inc." + +#endif /*GNUPG_MAJOR_VERSION*/ + +/* Replacements for standalone builds. */ +#ifndef GNUPG_MAJOR_VERSION +#ifndef _ +#define _(a) (a) +#endif +#ifndef DIM +#define DIM(v) (sizeof(v) / sizeof((v)[0])) +#endif +#define jnlib_malloc(a) malloc((a)) +#define jnlib_realloc(a, b) realloc((a), (b)) +#define jnlib_strdup(a) strdup((a)) +#define jnlib_free(a) free((a)) +#define jnlib_log_error my_log_error +#define jnlib_log_bug my_log_bug +#define trim_spaces(a) my_trim_spaces((a)) +#define map_static_macro_string(a) (a) +#endif /*!GNUPG_MAJOR_VERSION*/ + +#define ARGPARSE_STR(v) #v +#define ARGPARSE_STR2(v) ARGPARSE_STR(v) + +/* Replacements for standalone builds. */ +#ifndef GNUPG_MAJOR_VERSION +static void my_log_error(const char *fmt, ...) { + va_list arg_ptr; + + va_start(arg_ptr, fmt); + fprintf(stderr, "%s: ", strusage(11)); + vfprintf(stderr, fmt, arg_ptr); + va_end(arg_ptr); +} + +static void my_log_bug(const char *fmt, ...) { + va_list arg_ptr; + + va_start(arg_ptr, fmt); + fprintf(stderr, "%s: Ohhhh jeeee: ", strusage(11)); + vfprintf(stderr, fmt, arg_ptr); + va_end(arg_ptr); + abort(); +} + +static char *my_trim_spaces(char *str) { + char *string, *p, *mark; + + string = str; + /* Find first non space character. */ + for (p = string; *p && isspace(*(unsigned char *)p); p++) + ; + /* Move characters. */ + for ((mark = NULL); (*string = *p); string++, p++) + if (isspace(*(unsigned char *)p)) { + if (!mark) mark = string; + } else + mark = NULL; + if (mark) *mark = '\0'; /* Remove trailing spaces. */ + + return str; +} + +#endif /*!GNUPG_MAJOR_VERSION*/ + +/********************************* + * @Summary arg_parse + * #include "argparse.h" + * + * typedef struct { + * char *argc; pointer to argc (value subject to change) + * char ***argv; pointer to argv (value subject to change) + * unsigned flags; Global flags (DO NOT CHANGE) + * int err; print error about last option + * 1 = warning, 2 = abort + * int r_opt; return option + * int r_type; type of return value (0 = no argument found) + * union { + * int ret_int; + * long ret_long + * ulong ret_ulong; + * char *ret_str; + * } r; Return values + * struct { + * int idx; + * const char *last; + * void *aliases; + * } internal; DO NOT CHANGE + * } ARGPARSE_ARGS; + * + * typedef struct { + * int short_opt; + * const char *long_opt; + * unsigned flags; + * } ARGPARSE_OPTS; + * + * int arg_parse( ARGPARSE_ARGS *arg, ARGPARSE_OPTS *opts ); + * + * @Description + * This is my replacement for getopt(). See the example for a typical usage. + * Global flags are: + * Bit 0 : Do not remove options form argv + * Bit 1 : Do not stop at last option but return other args + * with r_opt set to -1. + * Bit 2 : Assume options and real args are mixed. + * Bit 3 : Do not use -- to stop option processing. + * Bit 4 : Do not skip the first arg. + * Bit 5 : allow usage of long option with only one dash + * Bit 6 : ignore --version + * all other bits must be set to zero, this value is modified by the + * function, so assume this is write only. + * Local flags (for each option): + * Bit 2-0 : 0 = does not take an argument + * 1 = takes int argument + * 2 = takes string argument + * 3 = takes long argument + * 4 = takes ulong argument + * Bit 3 : argument is optional (r_type will the be set to 0) + * Bit 4 : allow 0x etc. prefixed values. + * Bit 6 : Ignore this option + * Bit 7 : This is a command and not an option + * You stop the option processing by setting opts to NULL, the function will + * then return 0. + * @Return Value + * Returns the args.r_opt or 0 if ready + * r_opt may be -2/-7 to indicate an unknown option/command. + * @See Also + * ArgExpand + * @Notes + * You do not need to process the options 'h', '--help' or '--version' + * because this function includes standard help processing; but if you + * specify '-h', '--help' or '--version' you have to do it yourself. + * The option '--' stops argument processing; if bit 1 is set the function + * continues to return normal arguments. + * To process float args or unsigned args you must use a string args and do + * the conversion yourself. + * @Example + * + * ARGPARSE_OPTS opts[] = { + * { 'v', "verbose", 0 }, + * { 'd', "debug", 0 }, + * { 'o', "output", 2 }, + * { 'c', "cross-ref", 2|8 }, + * { 'm', "my-option", 1|8 }, + * { 300, "ignored-long-option, ARGPARSE_OP_IGNORE}, + * { 500, "have-no-short-option-for-this-long-option", 0 }, + * {0} }; + * ARGPARSE_ARGS pargs = { &argc, &argv, 0 } + * + * while( ArgParse( &pargs, &opts) ) { + * switch( pargs.r_opt ) { + * case 'v': opt.verbose++; break; + * case 'd': opt.debug++; break; + * case 'o': opt.outfile = pargs.r.ret_str; break; + * case 'c': opt.crf = pargs.r_type? pargs.r.ret_str:"a.crf"; break; + * case 'm': opt.myopt = pargs.r_type? pargs.r.ret_int : 1; break; + * case 500: opt.a_long_one++; break + * default : pargs.err = 1; break; -- force warning output -- + * } + * } + * if( argc > 1 ) + * log_fatal( "Too many args"); + * + */ + +typedef struct alias_def_s *ALIAS_DEF; +struct alias_def_s { + ALIAS_DEF next; + char *name; /* malloced buffer with name, \0, value */ + const char *value; /* ptr into name */ +}; + +/* Object to store the names for the --ignore-invalid-option option. + This is a simple linked list. */ +typedef struct iio_item_def_s *IIO_ITEM_DEF; +struct iio_item_def_s { + IIO_ITEM_DEF next; + char name[1]; /* String with the long option name. */ +}; + +static const char *(*strusage_handler)(int) = NULL; +static int (*custom_outfnc)(int, const char *); + +static int set_opt_arg(ARGPARSE_ARGS *arg, unsigned flags, char *s); +static void show_help(ARGPARSE_OPTS *opts, unsigned flags); +static void show_version(void); +static int writestrings(int is_error, const char *string, ...) +#if __GNUC__ >= 4 + __attribute__((sentinel(0))) +#endif + ; + +void argparse_register_outfnc(int (*fnc)(int, const char *)) { + custom_outfnc = fnc; +} + +/* Write STRING and all following const char * arguments either to + stdout or, if IS_ERROR is set, to stderr. The list of strings must + be terminated by a NULL. */ +static int writestrings(int is_error, const char *string, ...) { + va_list arg_ptr; + const char *s; + int count = 0; + + if (string) { + s = string; + va_start(arg_ptr, string); + do { + if (custom_outfnc) + custom_outfnc(is_error ? 2 : 1, s); + else + fputs(s, is_error ? stderr : stdout); + count += strlen(s); + } while ((s = va_arg(arg_ptr, const char *))); + va_end(arg_ptr); + } + return count; +} + +static void flushstrings(int is_error) { + if (custom_outfnc) + custom_outfnc(is_error ? 2 : 1, NULL); + else + fflush(is_error ? stderr : stdout); +} + +static void initialize(ARGPARSE_ARGS *arg, const char *filename, + unsigned *lineno) { + if (!(arg->flags & (1 << 15))) { + /* Initialize this instance. */ + arg->internal.idx = 0; + arg->internal.last = NULL; + arg->internal.inarg = 0; + arg->internal.stopped = 0; + arg->internal.aliases = NULL; + arg->internal.cur_alias = NULL; + arg->internal.iio_list = NULL; + arg->err = 0; + arg->flags |= 1 << 15; /* Mark as initialized. */ + if (*arg->argc < 0) jnlib_log_bug("invalid argument for arg_parse\n"); + } + + if (arg->err) { + /* Last option was erroneous. */ + const char *s; + + if (filename) { + if (arg->r_opt == ARGPARSE_UNEXPECTED_ARG) + s = _("argument not expected"); + else if (arg->r_opt == ARGPARSE_READ_ERROR) + s = _("read error"); + else if (arg->r_opt == ARGPARSE_KEYWORD_TOO_LONG) + s = _("keyword too long"); + else if (arg->r_opt == ARGPARSE_MISSING_ARG) + s = _("missing argument"); + else if (arg->r_opt == ARGPARSE_INVALID_ARG) + s = _("invalid argument"); + else if (arg->r_opt == ARGPARSE_INVALID_COMMAND) + s = _("invalid command"); + else if (arg->r_opt == ARGPARSE_INVALID_ALIAS) + s = _("invalid alias definition"); + else if (arg->r_opt == ARGPARSE_OUT_OF_CORE) + s = _("out of core"); + else + s = _("invalid option"); + jnlib_log_error("%s:%u: %s\n", filename, *lineno, s); + } else { + s = arg->internal.last ? arg->internal.last : "[??]"; + + if (arg->r_opt == ARGPARSE_MISSING_ARG) + jnlib_log_error(_("missing argument for option \"%.50s\"\n"), s); + else if (arg->r_opt == ARGPARSE_INVALID_ARG) + jnlib_log_error(_("invalid argument for option \"%.50s\"\n"), s); + else if (arg->r_opt == ARGPARSE_UNEXPECTED_ARG) + jnlib_log_error(_("option \"%.50s\" does not expect an " + "argument\n"), + s); + else if (arg->r_opt == ARGPARSE_INVALID_COMMAND) + jnlib_log_error(_("invalid command \"%.50s\"\n"), s); + else if (arg->r_opt == ARGPARSE_AMBIGUOUS_OPTION) + jnlib_log_error(_("option \"%.50s\" is ambiguous\n"), s); + else if (arg->r_opt == ARGPARSE_AMBIGUOUS_COMMAND) + jnlib_log_error(_("command \"%.50s\" is ambiguous\n"), s); + else if (arg->r_opt == ARGPARSE_OUT_OF_CORE) + jnlib_log_error("%s\n", _("out of core\n")); + else + jnlib_log_error(_("invalid option \"%.50s\"\n"), s); + } + if (arg->err != ARGPARSE_PRINT_WARNING) exit(2); + arg->err = 0; + } + + /* Zero out the return value union. */ + arg->r.ret_str = NULL; + arg->r.ret_long = 0; +} + +static void store_alias(ARGPARSE_ARGS *arg, char *name, char *value) { + /* TODO: replace this dummy function with a rea one + * and fix the probelms IRIX has with (ALIAS_DEV)arg.. + * used as lvalue + */ + (void)arg; + (void)name; + (void)value; +#if 0 + ALIAS_DEF a = jnlib_xmalloc( sizeof *a ); + a->name = name; + a->value = value; + a->next = (ALIAS_DEF)arg->internal.aliases; + (ALIAS_DEF)arg->internal.aliases = a; +#endif +} + +/* Return true if KEYWORD is in the ignore-invalid-option list. */ +static int ignore_invalid_option_p(ARGPARSE_ARGS *arg, const char *keyword) { + IIO_ITEM_DEF item = (IIO_ITEM_DEF)arg->internal.iio_list; + + for (; item; item = item->next) + if (!strcmp(item->name, keyword)) return 1; + return 0; +} + +/* Add the keywords up to the next LF to the list of to be ignored + options. After returning FP will either be at EOF or the next + character read wll be the first of a new line. The function + returns 0 on success or true on malloc failure. */ +static int ignore_invalid_option_add(ARGPARSE_ARGS *arg, FILE *fp) { + IIO_ITEM_DEF item; + int c; + char name[100]; + int namelen = 0; + int ready = 0; + enum { skipWS, collectNAME, skipNAME, addNAME } state = skipWS; + + while (!ready) { + c = getc(fp); + if (c == '\n') + ready = 1; + else if (c == EOF) { + c = '\n'; + ready = 1; + } + again: + switch (state) { + case skipWS: + if (!isascii(c) || !isspace(c)) { + namelen = 0; + state = collectNAME; + goto again; + } + break; + + case collectNAME: + if (isspace(c)) { + state = addNAME; + goto again; + } else if (namelen < DIM(name) - 1) + name[namelen++] = c; + else /* Too long. */ + state = skipNAME; + break; + + case skipNAME: + if (isspace(c)) { + state = skipWS; + goto again; + } + break; + + case addNAME: + name[namelen] = 0; + if (!ignore_invalid_option_p(arg, name)) { + item = (IIO_ITEM_DEF)jnlib_malloc(sizeof *item + namelen); + if (!item) return 1; + strcpy(item->name, name); + item->next = (IIO_ITEM_DEF)arg->internal.iio_list; + arg->internal.iio_list = item; + } + state = skipWS; + goto again; + } + } + return 0; +} + +/* Clear the entire ignore-invalid-option list. */ +static void ignore_invalid_option_clear(ARGPARSE_ARGS *arg) { + IIO_ITEM_DEF item, tmpitem; + + for (item = (IIO_ITEM_DEF)arg->internal.iio_list; item; item = tmpitem) { + tmpitem = item->next; + jnlib_free(item); + } + arg->internal.iio_list = NULL; +} + +/**************** + * Get options from a file. + * Lines starting with '#' are comment lines. + * Syntax is simply a keyword and the argument. + * Valid keywords are all keywords from the long_opt list without + * the leading dashes. The special keywords "help", "warranty" and "version" + * are not valid here. + * The special keyword "alias" may be used to store alias definitions, + * which are later expanded like long options. + * The option + * ignore-invalid-option OPTIONNAMEs + * is recognized and updates a list of option which should be ignored if they + * are not defined. + * Caller must free returned strings. + * If called with FP set to NULL command line args are parse instead. + * + * Q: Should we allow the syntax + * keyword = value + * and accept for boolean options a value of 1/0, yes/no or true/false? + * Note: Abbreviation of options is here not allowed. + */ +int optfile_parse(FILE *fp, const char *filename, unsigned *lineno, + ARGPARSE_ARGS *arg, ARGPARSE_OPTS *opts) { + int state, i, c; + int idx = 0; + char keyword[100]; + char *buffer = NULL; + size_t buflen = 0; + int in_alias = 0; + + if (!fp) /* Divert to to arg_parse() in this case. */ + return arg_parse(arg, opts); + + initialize(arg, filename, lineno); + + /* Find the next keyword. */ + state = i = 0; + for (;;) { + c = getc(fp); + if (c == '\n' || c == EOF) { + if (c != EOF) ++*lineno; + if (state == -1) + break; + else if (state == 2) { + keyword[i] = 0; + for (i = 0; opts[i].short_opt; i++) { + if (opts[i].long_opt && !strcmp(opts[i].long_opt, keyword)) break; + } + idx = i; + arg->r_opt = opts[idx].short_opt; + if ((opts[idx].flags & ARGPARSE_OPT_IGNORE)) { + state = i = 0; + continue; + } else if (!opts[idx].short_opt) { + if (!strcmp(keyword, "ignore-invalid-option")) { + /* No argument - ignore this meta option. */ + state = i = 0; + continue; + } else if (ignore_invalid_option_p(arg, keyword)) { + /* This invalid option is in the iio list. */ + state = i = 0; + continue; + } + arg->r_opt = ((opts[idx].flags & ARGPARSE_OPT_COMMAND) + ? ARGPARSE_INVALID_COMMAND + : ARGPARSE_INVALID_OPTION); + } else if (!(opts[idx].flags & ARGPARSE_TYPE_MASK)) + arg->r_type = 0; /* Does not take an arg. */ + else if ((opts[idx].flags & ARGPARSE_OPT_OPTIONAL)) + arg->r_type = 0; /* Arg is optional. */ + else + arg->r_opt = ARGPARSE_MISSING_ARG; + + break; + } else if (state == 3) { + /* No argument found. */ + if (in_alias) + arg->r_opt = ARGPARSE_MISSING_ARG; + else if (!(opts[idx].flags & ARGPARSE_TYPE_MASK)) + arg->r_type = 0; /* Does not take an arg. */ + else if ((opts[idx].flags & ARGPARSE_OPT_OPTIONAL)) + arg->r_type = 0; /* No optional argument. */ + else + arg->r_opt = ARGPARSE_MISSING_ARG; + + break; + } else if (state == 4) { + /* Has an argument. */ + if (in_alias) { + if (!buffer) + arg->r_opt = ARGPARSE_UNEXPECTED_ARG; + else { + char *p; + + buffer[i] = 0; + p = strpbrk(buffer, " \t"); + if (p) { + *p++ = 0; + trim_spaces(p); + } + if (!p || !*p) { + jnlib_free(buffer); + arg->r_opt = ARGPARSE_INVALID_ALIAS; + } else { + store_alias(arg, buffer, p); + } + } + } else if (!(opts[idx].flags & ARGPARSE_TYPE_MASK)) + arg->r_opt = ARGPARSE_UNEXPECTED_ARG; + else { + char *p; + + if (!buffer) { + keyword[i] = 0; + buffer = jnlib_strdup(keyword); + if (!buffer) arg->r_opt = ARGPARSE_OUT_OF_CORE; + } else + buffer[i] = 0; + + if (buffer) { + trim_spaces(buffer); + p = buffer; + if (*p == '"') { + /* Remove quotes. */ + p++; + if (*p && p[strlen(p) - 1] == '\"') p[strlen(p) - 1] = 0; + } + if (!set_opt_arg(arg, opts[idx].flags, p)) jnlib_free(buffer); + } + } + break; + } else if (c == EOF) { + ignore_invalid_option_clear(arg); + if (ferror(fp)) + arg->r_opt = ARGPARSE_READ_ERROR; + else + arg->r_opt = 0; /* EOF. */ + break; + } + state = 0; + i = 0; + } else if (state == -1) + ; /* Skip. */ + else if (state == 0 && isascii(c) && isspace(c)) + ; /* Skip leading white space. */ + else if (state == 0 && c == '#') + state = 1; /* Start of a comment. */ + else if (state == 1) + ; /* Skip comments. */ + else if (state == 2 && isascii(c) && isspace(c)) { + /* Check keyword. */ + keyword[i] = 0; + for (i = 0; opts[i].short_opt; i++) + if (opts[i].long_opt && !strcmp(opts[i].long_opt, keyword)) break; + idx = i; + arg->r_opt = opts[idx].short_opt; + if ((opts[idx].flags & ARGPARSE_OPT_IGNORE)) { + state = 1; /* Process like a comment. */ + } else if (!opts[idx].short_opt) { + if (!strcmp(keyword, "alias")) { + in_alias = 1; + state = 3; + } else if (!strcmp(keyword, "ignore-invalid-option")) { + if (ignore_invalid_option_add(arg, fp)) { + arg->r_opt = ARGPARSE_OUT_OF_CORE; + break; + } + state = i = 0; + ++*lineno; + } else if (ignore_invalid_option_p(arg, keyword)) + state = 1; /* Process like a comment. */ + else { + arg->r_opt = ((opts[idx].flags & ARGPARSE_OPT_COMMAND) + ? ARGPARSE_INVALID_COMMAND + : ARGPARSE_INVALID_OPTION); + state = -1; /* Skip rest of line and leave. */ + } + } else + state = 3; + } else if (state == 3) { + /* Skip leading spaces of the argument. */ + if (!isascii(c) || !isspace(c)) { + i = 0; + keyword[i++] = c; + state = 4; + } + } else if (state == 4) { + /* Collect the argument. */ + if (buffer) { + if (i < buflen - 1) + buffer[i++] = c; + else { + char *tmp; + size_t tmplen = buflen + 50; + + tmp = (char *)jnlib_realloc(buffer, tmplen); + if (tmp) { + buflen = tmplen; + buffer = tmp; + buffer[i++] = c; + } else { + jnlib_free(buffer); + arg->r_opt = ARGPARSE_OUT_OF_CORE; + break; + } + } + } else if (i < DIM(keyword) - 1) + keyword[i++] = c; + else { + size_t tmplen = DIM(keyword) + 50; + buffer = (char *)jnlib_malloc(tmplen); + if (buffer) { + buflen = tmplen; + memcpy(buffer, keyword, i); + buffer[i++] = c; + } else { + arg->r_opt = ARGPARSE_OUT_OF_CORE; + break; + } + } + } else if (i >= DIM(keyword) - 1) { + arg->r_opt = ARGPARSE_KEYWORD_TOO_LONG; + state = -1; /* Skip rest of line and leave. */ + } else { + keyword[i++] = c; + state = 2; + } + } + + return arg->r_opt; +} + +static int find_long_option(ARGPARSE_ARGS *arg, ARGPARSE_OPTS *opts, + const char *keyword) { + int i; + size_t n; + + (void)arg; + + /* Would be better if we can do a binary search, but it is not + possible to reorder our option table because we would mess + up our help strings - What we can do is: Build a nice option + lookup table when this function is first invoked */ + if (!*keyword) return -1; + for (i = 0; opts[i].short_opt; i++) + if (opts[i].long_opt && !strcmp(opts[i].long_opt, keyword)) return i; +#if 0 + { + ALIAS_DEF a; + /* see whether it is an alias */ + for( a = args->internal.aliases; a; a = a->next ) { + if( !strcmp( a->name, keyword) ) { + /* todo: must parse the alias here */ + args->internal.cur_alias = a; + return -3; /* alias available */ + } + } + } +#endif + /* not found, see whether it is an abbreviation */ + /* aliases may not be abbreviated */ + n = strlen(keyword); + for (i = 0; opts[i].short_opt; i++) { + if (opts[i].long_opt && !strncmp(opts[i].long_opt, keyword, n)) { + int j; + for (j = i + 1; opts[j].short_opt; j++) { + if (opts[j].long_opt && !strncmp(opts[j].long_opt, keyword, n)) + return -2; /* abbreviation is ambiguous */ + } + return i; + } + } + return -1; /* Not found. */ +} + +int arg_parse(ARGPARSE_ARGS *arg, ARGPARSE_OPTS *opts) { + int idx; + int argc; + char **argv; + char *s, *s2; + int i; + + initialize(arg, NULL, NULL); + argc = *arg->argc; + argv = *arg->argv; + idx = arg->internal.idx; + + if (!idx && argc && !(arg->flags & ARGPARSE_FLAG_ARG0)) { + /* Skip the first argument. */ + argc--; + argv++; + idx++; + } + +next_one: + if (!argc) { + /* No more args. */ + arg->r_opt = 0; + goto leave; /* Ready. */ + } + + s = *argv; + arg->internal.last = s; + + if (arg->internal.stopped && (arg->flags & ARGPARSE_FLAG_ALL)) { + arg->r_opt = ARGPARSE_IS_ARG; /* Not an option but an argument. */ + arg->r_type = 2; + arg->r.ret_str = s; + argc--; + argv++; + idx++; /* set to next one */ + } else if (arg->internal.stopped) { + arg->r_opt = 0; + goto leave; /* Ready. */ + } else if (*s == '-' && s[1] == '-') { + /* Long option. */ + char *argpos; + + arg->internal.inarg = 0; + if (!s[2] && !(arg->flags & ARGPARSE_FLAG_NOSTOP)) { + /* Stop option processing. */ + arg->internal.stopped = 1; + arg->flags |= ARGPARSE_FLAG_STOP_SEEN; + argc--; + argv++; + idx++; + goto next_one; + } + + argpos = strchr(s + 2, '='); + if (argpos) *argpos = 0; + i = find_long_option(arg, opts, s + 2); + if (argpos) *argpos = '='; + + if (i < 0 && !strcmp("help", s + 2)) + show_help(opts, arg->flags); + else if (i < 0 && !strcmp("version", s + 2)) { + if (!(arg->flags & ARGPARSE_FLAG_NOVERSION)) { + show_version(); + exit(0); + } + } else if (i < 0 && !strcmp("warranty", s + 2)) { + writestrings(0, strusage(16), "\n", NULL); + exit(0); + } else if (i < 0 && !strcmp("dump-options", s + 2)) { + for (i = 0; opts[i].short_opt; i++) { + if (opts[i].long_opt && !(opts[i].flags & ARGPARSE_OPT_IGNORE)) + writestrings(0, "--", opts[i].long_opt, "\n", NULL); + } + writestrings(0, "--dump-options\n--help\n--version\n--warranty\n", NULL); + exit(0); + } + + if (i == -2) + arg->r_opt = ARGPARSE_AMBIGUOUS_OPTION; + else if (i == -1) { + arg->r_opt = ARGPARSE_INVALID_OPTION; + arg->r.ret_str = s + 2; + } else + arg->r_opt = opts[i].short_opt; + if (i < 0) + ; + else if ((opts[i].flags & ARGPARSE_TYPE_MASK)) { + if (argpos) { + s2 = argpos + 1; + if (!*s2) s2 = NULL; + } else + s2 = argv[1]; + if (!s2 && (opts[i].flags & ARGPARSE_OPT_OPTIONAL)) { + arg->r_type = ARGPARSE_TYPE_NONE; /* Argument is optional. */ + } else if (!s2) { + arg->r_opt = ARGPARSE_MISSING_ARG; + } else if (!argpos && *s2 == '-' && + (opts[i].flags & ARGPARSE_OPT_OPTIONAL)) { + /* The argument is optional and the next seems to be an + option. We do not check this possible option but + assume no argument */ + arg->r_type = ARGPARSE_TYPE_NONE; + } else { + set_opt_arg(arg, opts[i].flags, s2); + if (!argpos) { + argc--; + argv++; + idx++; /* Skip one. */ + } + } + } else { + /* Does not take an argument. */ + if (argpos) + arg->r_type = ARGPARSE_UNEXPECTED_ARG; + else + arg->r_type = 0; + } + argc--; + argv++; + idx++; /* Set to next one. */ + } else if ((*s == '-' && s[1]) || arg->internal.inarg) { + /* Short option. */ + int dash_kludge = 0; + + i = 0; + if (!arg->internal.inarg) { + arg->internal.inarg++; + if ((arg->flags & ARGPARSE_FLAG_ONEDASH)) { + for (i = 0; opts[i].short_opt; i++) + if (opts[i].long_opt && !strcmp(opts[i].long_opt, s + 1)) { + dash_kludge = 1; + break; + } + } + } + s += arg->internal.inarg; + + if (!dash_kludge) { + for (i = 0; opts[i].short_opt; i++) + if (opts[i].short_opt == *s) break; + } + + if (!opts[i].short_opt && (*s == 'h' || *s == '?')) + show_help(opts, arg->flags); + + arg->r_opt = opts[i].short_opt; + if (!opts[i].short_opt) { + arg->r_opt = (opts[i].flags & ARGPARSE_OPT_COMMAND) + ? ARGPARSE_INVALID_COMMAND + : ARGPARSE_INVALID_OPTION; + arg->internal.inarg++; /* Point to the next arg. */ + arg->r.ret_str = s; + } else if ((opts[i].flags & ARGPARSE_TYPE_MASK)) { + if (s[1] && !dash_kludge) { + s2 = s + 1; + set_opt_arg(arg, opts[i].flags, s2); + } else { + s2 = argv[1]; + if (!s2 && (opts[i].flags & ARGPARSE_OPT_OPTIONAL)) { + arg->r_type = ARGPARSE_TYPE_NONE; + } else if (!s2) { + arg->r_opt = ARGPARSE_MISSING_ARG; + } else if (*s2 == '-' && s2[1] && + (opts[i].flags & ARGPARSE_OPT_OPTIONAL)) { + /* The argument is optional and the next seems to + be an option. We do not check this possible + option but assume no argument. */ + arg->r_type = ARGPARSE_TYPE_NONE; + } else { + set_opt_arg(arg, opts[i].flags, s2); + argc--; + argv++; + idx++; /* Skip one. */ + } + } + s = "x"; /* This is so that !s[1] yields false. */ + } else { + /* Does not take an argument. */ + arg->r_type = ARGPARSE_TYPE_NONE; + arg->internal.inarg++; /* Point to the next arg. */ + } + if (!s[1] || dash_kludge) { + /* No more concatenated short options. */ + arg->internal.inarg = 0; + argc--; + argv++; + idx++; + } + } else if (arg->flags & ARGPARSE_FLAG_MIXED) { + arg->r_opt = ARGPARSE_IS_ARG; + arg->r_type = 2; + arg->r.ret_str = s; + argc--; + argv++; + idx++; /* Set to next one. */ + } else { + arg->internal.stopped = 1; /* Stop option processing. */ + goto next_one; + } + +leave: + *arg->argc = argc; + *arg->argv = argv; + arg->internal.idx = idx; + return arg->r_opt; +} + +/* Returns: -1 on error, 0 for an integer type and 1 for a non integer + type argument. */ +static int set_opt_arg(ARGPARSE_ARGS *arg, unsigned flags, char *s) { + int base = (flags & ARGPARSE_OPT_PREFIX) ? 0 : 10; + long l; + + switch ((arg->r_type = (flags & ARGPARSE_TYPE_MASK))) { + case ARGPARSE_TYPE_LONG: + case ARGPARSE_TYPE_INT: + errno = 0; + l = strtol(s, NULL, base); + if ((l == LONG_MIN || l == LONG_MAX) && errno == ERANGE) { + arg->r_opt = ARGPARSE_INVALID_ARG; + return -1; + } + if (arg->r_type == ARGPARSE_TYPE_LONG) + arg->r.ret_long = l; + else if ((l < 0 && l < INT_MIN) || l > INT_MAX) { + arg->r_opt = ARGPARSE_INVALID_ARG; + return -1; + } else + arg->r.ret_int = (int)l; + return 0; + + case ARGPARSE_TYPE_ULONG: + while (isascii(*s) && isspace(*s)) s++; + if (*s == '-') { + arg->r.ret_ulong = 0; + arg->r_opt = ARGPARSE_INVALID_ARG; + return -1; + } + errno = 0; + arg->r.ret_ulong = strtoul(s, NULL, base); + if (arg->r.ret_ulong == ULONG_MAX && errno == ERANGE) { + arg->r_opt = ARGPARSE_INVALID_ARG; + return -1; + } + return 0; + + case ARGPARSE_TYPE_STRING: + default: + arg->r.ret_str = s; + return 1; + } +} + +static size_t long_opt_strlen(ARGPARSE_OPTS *o) { + size_t n = strlen(o->long_opt); + + if (o->description && *o->description == '|') { + const char *s; +#ifdef JNLIB_NEED_UTF8CONV + int is_utf8 = is_native_utf8(); +#endif + + s = o->description + 1; + if (*s != '=') n++; + /* For a (mostly) correct length calculation we exclude + continuation bytes (10xxxxxx) if we are on a native utf8 + terminal. */ + for (; *s && *s != '|'; s++) +#ifdef JNLIB_NEED_UTF8CONV + if (is_utf8 && (*s & 0xc0) != 0x80) +#endif + n++; + } + return n; +} + +/**************** + * Print formatted help. The description string has some special + * meanings: + * - A description string which is "@" suppresses help output for + * this option + * - a description,ine which starts with a '@' and is followed by + * any other characters is printed as is; this may be used for examples + * ans such. + * - A description which starts with a '|' outputs the string between this + * bar and the next one as arguments of the long option. + */ +static void show_help(ARGPARSE_OPTS *opts, unsigned int flags) { + const char *s; + char tmp[2]; + + show_version(); + writestrings(0, "\n", NULL); + s = strusage(42); + if (s && *s == '1') { + s = strusage(40); + writestrings(1, s, NULL); + if (*s && s[strlen(s)] != '\n') writestrings(1, "\n", NULL); + } + s = strusage(41); + writestrings(0, s, "\n", NULL); + if (opts[0].description) { + /* Auto format the option description. */ + int i, j, indent; + + /* Get max. length of long options. */ + for (i = indent = 0; opts[i].short_opt; i++) { + if (opts[i].long_opt) + if (!opts[i].description || *opts[i].description != '@') + if ((j = long_opt_strlen(opts + i)) > indent && j < 35) indent = j; + } + + /* Example: " -v, --verbose Viele Sachen ausgeben" */ + indent += 10; + if (*opts[0].description != '@') writestrings(0, "Options:", "\n", NULL); + for (i = 0; opts[i].short_opt; i++) { + s = map_static_macro_string(_(opts[i].description)); + if (s && *s == '@' && !s[1]) /* Hide this line. */ + continue; + if (s && *s == '@') /* Unindented comment only line. */ + { + for (s++; *s; s++) { + if (*s == '\n') { + if (s[1]) writestrings(0, "\n", NULL); + } else { + tmp[0] = *s; + tmp[1] = 0; + writestrings(0, tmp, NULL); + } + } + writestrings(0, "\n", NULL); + continue; + } + + j = 3; + if (opts[i].short_opt < 256) { + tmp[0] = opts[i].short_opt; + tmp[1] = 0; + writestrings(0, " -", tmp, NULL); + if (!opts[i].long_opt) { + if (s && *s == '|') { + writestrings(0, " ", NULL); + j++; + for (s++; *s && *s != '|'; s++, j++) { + tmp[0] = *s; + tmp[1] = 0; + writestrings(0, tmp, NULL); + } + if (*s) s++; + } + } + } else + writestrings(0, " ", NULL); + if (opts[i].long_opt) { + tmp[0] = opts[i].short_opt < 256 ? ',' : ' '; + tmp[1] = 0; + j += writestrings(0, tmp, " --", opts[i].long_opt, NULL); + if (s && *s == '|') { + if (*++s != '=') { + writestrings(0, " ", NULL); + j++; + } + for (; *s && *s != '|'; s++, j++) { + tmp[0] = *s; + tmp[1] = 0; + writestrings(0, tmp, NULL); + } + if (*s) s++; + } + writestrings(0, " ", NULL); + j += 3; + } + for (; j < indent; j++) writestrings(0, " ", NULL); + if (s) { + if (*s && j > indent) { + writestrings(0, "\n", NULL); + for (j = 0; j < indent; j++) writestrings(0, " ", NULL); + } + for (; *s; s++) { + if (*s == '\n') { + if (s[1]) { + writestrings(0, "\n", NULL); + for (j = 0; j < indent; j++) writestrings(0, " ", NULL); + } + } else { + tmp[0] = *s; + tmp[1] = 0; + writestrings(0, tmp, NULL); + } + } + } + writestrings(0, "\n", NULL); + } + if ((flags & ARGPARSE_FLAG_ONEDASH)) + writestrings(0, + "\n(A single dash may be used " + "instead of the double ones)\n", + NULL); + } + if ((s = strusage(19))) { + writestrings(0, "\n", NULL); + writestrings(0, s, NULL); + } + flushstrings(0); + exit(0); +} + +static void show_version(void) { + const char *s; + int i; + + /* Version line. */ + writestrings(0, strusage(11), NULL); + if ((s = strusage(12))) writestrings(0, " (", s, ")", NULL); + writestrings(0, " ", strusage(13), "\n", NULL); + /* Additional version lines. */ + for (i = 20; i < 30; i++) + if ((s = strusage(i))) writestrings(0, s, "\n", NULL); + /* Copyright string. */ + if ((s = strusage(14))) writestrings(0, s, "\n", NULL); + /* Licence string. */ + if ((s = strusage(10))) writestrings(0, s, "\n", NULL); + /* Copying conditions. */ + if ((s = strusage(15))) writestrings(0, s, NULL); + /* Thanks. */ + if ((s = strusage(18))) writestrings(0, s, NULL); + /* Additional program info. */ + for (i = 30; i < 40; i++) + if ((s = strusage(i))) writestrings(0, s, NULL); + flushstrings(0); +} + +void usage(int level) { + const char *p; + + if (!level) { + writestrings(1, strusage(11), " ", strusage(13), "; ", strusage(14), "\n", + NULL); + flushstrings(1); + } else if (level == 1) { + p = strusage(40); + writestrings(1, p, NULL); + if (*p && p[strlen(p)] != '\n') writestrings(1, "\n", NULL); + exit(2); + } else if (level == 2) { + p = strusage(42); + if (p && *p == '1') { + p = strusage(40); + writestrings(1, p, NULL); + if (*p && p[strlen(p)] != '\n') writestrings(1, "\n", NULL); + } + writestrings(0, strusage(41), "\n", NULL); + exit(0); + } +} + +/* Level + * 0: Print copyright string to stderr + * 1: Print a short usage hint to stderr and terminate + * 2: Print a long usage hint to stdout and terminate + * 10: Return license info string + * 11: Return the name of the program + * 12: Return optional name of package which includes this program. + * 13: version string + * 14: copyright string + * 15: Short copying conditions (with LFs) + * 16: Long copying conditions (with LFs) + * 17: Optional printable OS name + * 18: Optional thanks list (with LFs) + * 19: Bug report info + *20..29: Additional lib version strings. + *30..39: Additional program info (with LFs) + * 40: short usage note (with LF) + * 41: long usage note (with LF) + * 42: Flag string: + * First char is '1': + * The short usage notes needs to be printed + * before the long usage note. + */ +const char *strusage(int level) { + const char *p = strusage_handler ? strusage_handler(level) : NULL; + + if (p) return map_static_macro_string(p); + + switch (level) { + case 10: +#if ARGPARSE_GPL_VERSION == 3 + p = + ("License GPLv3+: GNU GPL version 3 or later " + "<https://www.gnu.org/licenses/gpl.html>"); +#else + p = + ("License GPLv2+: GNU GPL version 2 or later " + "<https://www.gnu.org/licenses/>"); +#endif + break; + case 11: + p = "foo"; + break; + case 13: + p = "0.0"; + break; + case 14: + p = ARGPARSE_CRIGHT_STR; + break; + case 15: + p = "This is free software: you are free to change and redistribute it.\n" + "There is NO WARRANTY, to the extent permitted by law.\n"; + break; + case 16: + p = +"This is free software; you can redistribute it and/or modify\n" +"it under the terms of the GNU General Public License as published by\n" +"the Free Software Foundation; either version " +ARGPARSE_STR2(ARGPARSE_GPL_VERSION) +" of the License, or\n" +"(at your option) any later version.\n\n" +"It is distributed in the hope that it will be useful,\n" +"but WITHOUT ANY WARRANTY; without even the implied warranty of\n" +"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" +"GNU General Public License for more details.\n\n" +"You should have received a copy of the GNU General Public License\n" +"along with this software. If not, see <https://www.gnu.org/licenses/>.\n"; + break; + case 40: /* short and long usage */ + case 41: + p = ""; + break; + } + + return p; +} + +/* Set the usage handler. This function is basically a constructor. */ +void set_strusage(const char *(*f)(int)) { strusage_handler = f; } + +#ifdef TEST +static struct { + int verbose; + int debug; + char *outfile; + char *crf; + int myopt; + int echo; + int a_long_one; +} opt; + +int main(int argc, char **argv) { + ARGPARSE_OPTS opts[] = { + ARGPARSE_x('v', "verbose", NONE, 0, "Laut sein"), + ARGPARSE_s_n('e', "echo", + ("Zeile ausgeben, damit wir sehen, " + "was wir eingegeben haben")), + ARGPARSE_s_n('d', "debug", "Debug\nfalls mal etwas\nschief geht"), + ARGPARSE_s_s('o', "output", 0), + ARGPARSE_o_s('c', "cross-ref", "cross-reference erzeugen\n"), + /* Note that on a non-utf8 terminal the ß might garble the output. */ + ARGPARSE_s_n('s', "street", + "|Straße|set the name of the street to Straße"), + ARGPARSE_o_i('m', "my-option", 0), ARGPARSE_s_n(500, "a-long-option", 0), + ARGPARSE_end()}; + ARGPARSE_ARGS pargs = { + &argc, &argv, + (ARGPARSE_FLAG_ALL | ARGPARSE_FLAG_MIXED | ARGPARSE_FLAG_ONEDASH)}; + int i; + + while (arg_parse(&pargs, opts)) { + switch (pargs.r_opt) { + case ARGPARSE_IS_ARG: + printf("arg='%s'\n", pargs.r.ret_str); + break; + case 'v': + opt.verbose++; + break; + case 'e': + opt.echo++; + break; + case 'd': + opt.debug++; + break; + case 'o': + opt.outfile = pargs.r.ret_str; + break; + case 'c': + opt.crf = pargs.r_type ? pargs.r.ret_str : "a.crf"; + break; + case 'm': + opt.myopt = pargs.r_type ? pargs.r.ret_int : 1; + break; + case 500: + opt.a_long_one++; + break; + default: + pargs.err = ARGPARSE_PRINT_WARNING; + break; + } + } + for (i = 0; i < argc; i++) printf("%3d -> (%s)\n", i, argv[i]); + puts("Options:"); + if (opt.verbose) printf(" verbose=%d\n", opt.verbose); + if (opt.debug) printf(" debug=%d\n", opt.debug); + if (opt.outfile) printf(" outfile='%s'\n", opt.outfile); + if (opt.crf) printf(" crffile='%s'\n", opt.crf); + if (opt.myopt) printf(" myopt=%d\n", opt.myopt); + if (opt.a_long_one) printf(" a-long-one=%d\n", opt.a_long_one); + if (opt.echo) printf(" echo=%d\n", opt.echo); + + return 0; +} +#endif /*TEST*/ + +/**** bottom of file ****/ diff --git a/src/pinentry/argparse.h b/src/pinentry/argparse.h new file mode 100644 index 00000000..5b652eba --- /dev/null +++ b/src/pinentry/argparse.h @@ -0,0 +1,204 @@ +/* argparse.h - Argument parser for option handling. + * Copyright (C) 1998,1999,2000,2001,2006 Free Software Foundation, Inc. + * + * This file is part of JNLIB, which is a subsystem of GnuPG. + * + * JNLIB is free software; you can redistribute it and/or modify it + * under the terms of either + * + * - the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at + * your option) any later version. + * + * or + * + * - the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * or both in parallel, as here. + * + * JNLIB 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 copies of the GNU General Public License + * and the GNU Lesser General Public License along with this program; + * if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: (GPL-2.0+ OR LGPL-3.0+) + */ + +#ifndef LIBJNLIB_ARGPARSE_H +#define LIBJNLIB_ARGPARSE_H + +#include <stdio.h> + +typedef struct +{ + int *argc; /* Pointer to ARGC (value subject to change). */ + char ***argv; /* Pointer to ARGV (value subject to change). */ + unsigned int flags; /* Global flags. May be set prior to calling the + parser. The parser may change the value. */ + int err; /* Print error description for last option. + Either 0, ARGPARSE_PRINT_WARNING or + ARGPARSE_PRINT_ERROR. */ + + int r_opt; /* Returns option code. */ + int r_type; /* Returns type of option value. */ + union { + int ret_int; + long ret_long; + unsigned long ret_ulong; + char *ret_str; + } r; /* Return values */ + + struct { + int idx; + int inarg; + int stopped; + const char *last; + void *aliases; + const void *cur_alias; + void *iio_list; + } internal; /* Private - do not change. */ +} ARGPARSE_ARGS; + +typedef struct +{ + int short_opt; + const char *long_opt; + unsigned int flags; + const char *description; /* Optional option description. */ +} ARGPARSE_OPTS; + + +/* Global flags (ARGPARSE_ARGS). */ +#define ARGPARSE_FLAG_KEEP 1 /* Do not remove options form argv. */ +#define ARGPARSE_FLAG_ALL 2 /* Do not stop at last option but return + remaining args with R_OPT set to -1. */ +#define ARGPARSE_FLAG_MIXED 4 /* Assume options and args are mixed. */ +#define ARGPARSE_FLAG_NOSTOP 8 /* Do not stop processing at "--". */ +#define ARGPARSE_FLAG_ARG0 16 /* Do not skip the first arg. */ +#define ARGPARSE_FLAG_ONEDASH 32 /* Allow long options with one dash. */ +#define ARGPARSE_FLAG_NOVERSION 64 /* No output for "--version". */ + +#define ARGPARSE_FLAG_STOP_SEEN 256 /* Set to true if a "--" has been seen. */ + +/* Flags for each option (ARGPARSE_OPTS). The type code may be + ORed with the OPT flags. */ +#define ARGPARSE_TYPE_NONE 0 /* Does not take an argument. */ +#define ARGPARSE_TYPE_INT 1 /* Takes an int argument. */ +#define ARGPARSE_TYPE_STRING 2 /* Takes a string argument. */ +#define ARGPARSE_TYPE_LONG 3 /* Takes a long argument. */ +#define ARGPARSE_TYPE_ULONG 4 /* Takes an unsigned long argument. */ +#define ARGPARSE_OPT_OPTIONAL (1<<3) /* Argument is optional. */ +#define ARGPARSE_OPT_PREFIX (1<<4) /* Allow 0x etc. prefixed values. */ +#define ARGPARSE_OPT_IGNORE (1<<6) /* Ignore command or option. */ +#define ARGPARSE_OPT_COMMAND (1<<7) /* The argument is a command. */ + +#define ARGPARSE_TYPE_MASK 7 /* Mask for the type values (internal). */ + +/* A set of macros to make option definitions easier to read. */ +#define ARGPARSE_x(s,l,t,f,d) \ + { (s), (l), ARGPARSE_TYPE_ ## t | (f), (d) } + +#define ARGPARSE_s(s,l,t,d) \ + { (s), (l), ARGPARSE_TYPE_ ## t, (d) } +#define ARGPARSE_s_n(s,l,d) \ + { (s), (l), ARGPARSE_TYPE_NONE, (d) } +#define ARGPARSE_s_i(s,l,d) \ + { (s), (l), ARGPARSE_TYPE_INT, (d) } +#define ARGPARSE_s_s(s,l,d) \ + { (s), (l), ARGPARSE_TYPE_STRING, (d) } +#define ARGPARSE_s_l(s,l,d) \ + { (s), (l), ARGPARSE_TYPE_LONG, (d) } +#define ARGPARSE_s_u(s,l,d) \ + { (s), (l), ARGPARSE_TYPE_ULONG, (d) } + +#define ARGPARSE_o(s,l,t,d) \ + { (s), (l), (ARGPARSE_TYPE_ ## t | ARGPARSE_OPT_OPTIONAL), (d) } +#define ARGPARSE_o_n(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_NONE | ARGPARSE_OPT_OPTIONAL), (d) } +#define ARGPARSE_o_i(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_INT | ARGPARSE_OPT_OPTIONAL), (d) } +#define ARGPARSE_o_s(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_STRING | ARGPARSE_OPT_OPTIONAL), (d) } +#define ARGPARSE_o_l(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_LONG | ARGPARSE_OPT_OPTIONAL), (d) } +#define ARGPARSE_o_u(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_ULONG | ARGPARSE_OPT_OPTIONAL), (d) } + +#define ARGPARSE_p(s,l,t,d) \ + { (s), (l), (ARGPARSE_TYPE_ ## t | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_p_n(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_NONE | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_p_i(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_INT | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_p_s(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_STRING | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_p_l(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_LONG | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_p_u(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_ULONG | ARGPARSE_OPT_PREFIX), (d) } + +#define ARGPARSE_op(s,l,t,d) \ + { (s), (l), (ARGPARSE_TYPE_ ## t \ + | ARGPARSE_OPT_OPTIONAL | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_op_n(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_NONE \ + | ARGPARSE_OPT_OPTIONAL | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_op_i(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_INT \ + | ARGPARSE_OPT_OPTIONAL | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_op_s(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_STRING \ + | ARGPARSE_OPT_OPTIONAL | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_op_l(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_LONG \ + | ARGPARSE_OPT_OPTIONAL | ARGPARSE_OPT_PREFIX), (d) } +#define ARGPARSE_op_u(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_ULONG \ + | ARGPARSE_OPT_OPTIONAL | ARGPARSE_OPT_PREFIX), (d) } + +#define ARGPARSE_c(s,l,d) \ + { (s), (l), (ARGPARSE_TYPE_NONE | ARGPARSE_OPT_COMMAND), (d) } + +#define ARGPARSE_ignore(s,l) \ + { (s), (l), (ARGPARSE_OPT_IGNORE), "@" } + +#define ARGPARSE_group(s,d) \ + { (s), NULL, 0, (d) } + +#define ARGPARSE_end() { 0, NULL, 0, NULL } + + +/* Other constants. */ +#define ARGPARSE_PRINT_WARNING 1 +#define ARGPARSE_PRINT_ERROR 2 + + +/* Error values. */ +#define ARGPARSE_IS_ARG (-1) +#define ARGPARSE_INVALID_OPTION (-2) +#define ARGPARSE_MISSING_ARG (-3) +#define ARGPARSE_KEYWORD_TOO_LONG (-4) +#define ARGPARSE_READ_ERROR (-5) +#define ARGPARSE_UNEXPECTED_ARG (-6) +#define ARGPARSE_INVALID_COMMAND (-7) +#define ARGPARSE_AMBIGUOUS_OPTION (-8) +#define ARGPARSE_AMBIGUOUS_COMMAND (-9) +#define ARGPARSE_INVALID_ALIAS (-10) +#define ARGPARSE_OUT_OF_CORE (-11) +#define ARGPARSE_INVALID_ARG (-12) + + +int arg_parse (ARGPARSE_ARGS *arg, ARGPARSE_OPTS *opts); +int optfile_parse (FILE *fp, const char *filename, unsigned *lineno, + ARGPARSE_ARGS *arg, ARGPARSE_OPTS *opts); +void usage (int level); +const char *strusage (int level); +void set_strusage (const char *(*f)( int )); +void argparse_register_outfnc (int (*fnc)(int, const char *)); + +#endif /*LIBJNLIB_ARGPARSE_H*/ diff --git a/src/pinentry/capslock/capslock.cpp b/src/pinentry/capslock/capslock.cpp new file mode 100644 index 00000000..3ff0b779 --- /dev/null +++ b/src/pinentry/capslock/capslock.cpp @@ -0,0 +1,45 @@ +/* capslock.cpp - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <QDebug> +#include <QGuiApplication> + +#include "capslock.h" + +CapsLockWatcher::Private::Private(CapsLockWatcher *q) : q{q} { +#ifdef PINENTRY_QT_WAYLAND + if (qApp->platformName() == QLatin1String("wayland")) { + watchWayland(); + } +#endif +} + +CapsLockWatcher::CapsLockWatcher(QObject *parent) + : QObject{parent}, d{new Private{this}} { + if (qApp->platformName() == QLatin1String("wayland")) { +#ifndef PINENTRY_QT_WAYLAND + qWarning() << "CapsLockWatcher was compiled without support for Wayland"; +#endif + } +} diff --git a/src/pinentry/capslock/capslock.h b/src/pinentry/capslock/capslock.h new file mode 100644 index 00000000..138f88cc --- /dev/null +++ b/src/pinentry/capslock/capslock.h @@ -0,0 +1,77 @@ +/* capslock.h - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_CAPSLOCK_H__ +#define __PINENTRY_QT_CAPSLOCK_H__ + +#include <QObject> +#include <memory> + +enum class LockState { Unknown = -1, Off, On }; + +LockState capsLockState(); + +#ifdef PINENTRY_QT_WAYLAND +namespace KWayland { +namespace Client { +class Registry; +class Seat; +} // namespace Client +} // namespace KWayland +#endif + +class CapsLockWatcher : public QObject { + Q_OBJECT + + public: + explicit CapsLockWatcher(QObject *parent = nullptr); + + Q_SIGNALS: + void stateChanged(bool locked); + + private: + class Private; + std::unique_ptr<Private> d; +}; + +class CapsLockWatcher::Private { + public: + explicit Private(CapsLockWatcher *); +#ifdef PINENTRY_QT_WAYLAND + void watchWayland(); +#endif + + private: +#ifdef PINENTRY_QT_WAYLAND + void registry_seatAnnounced(quint32, quint32); + void seat_hasKeyboardChanged(bool); + void keyboard_modifiersChanged(quint32); +#endif + + private: + CapsLockWatcher *const q; + +#ifdef PINENTRY_QT_WAYLAND + KWayland::Client::Registry *registry = nullptr; + KWayland::Client::Seat *seat = nullptr; +#endif +}; + +#endif // __PINENTRY_QT_CAPSLOCK_H__ diff --git a/src/pinentry/capslock/capslock_unix.cpp b/src/pinentry/capslock/capslock_unix.cpp new file mode 100644 index 00000000..e4f4cd17 --- /dev/null +++ b/src/pinentry/capslock/capslock_unix.cpp @@ -0,0 +1,137 @@ +/* capslock_unix.cpp - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "capslock.h" + +#ifdef PINENTRY_QT_WAYLAND +#include <KWayland/Client/connection_thread.h> +#include <KWayland/Client/keyboard.h> +#include <KWayland/Client/registry.h> +#include <KWayland/Client/seat.h> +#endif + +#include <QGuiApplication> + +#ifdef PINENTRY_QT_X11 +#include <X11/XKBlib.h> + +#include <QX11Info> +#undef Status +#endif + +#include <QDebug> + +#ifdef PINENTRY_QT_WAYLAND +using namespace KWayland::Client; +#endif + +#ifdef PINENTRY_QT_WAYLAND +static bool watchingWayland = false; +#endif + +LockState capsLockState() { + static bool reportUnsupportedPlatform = true; +#ifdef PINENTRY_QT_X11 + if (qApp->platformName() == QLatin1String("xcb")) { + unsigned int state; + XkbGetIndicatorState(QX11Info::display(), XkbUseCoreKbd, &state); + return (state & 0x01) == 1 ? LockState::On : LockState::Off; + } +#endif +#ifdef PINENTRY_QT_WAYLAND + if (qApp->platformName() == QLatin1String("wayland")) { + if (!watchingWayland && reportUnsupportedPlatform) { + qDebug() << "Use CapsLockWatcher for checking for Caps Lock on Wayland"; + } + } else +#endif + if (reportUnsupportedPlatform) { + qWarning() << "Checking for Caps Lock not possible on unsupported platform:" + << qApp->platformName(); + } + reportUnsupportedPlatform = false; + return LockState::Unknown; +} + +#ifdef PINENTRY_QT_WAYLAND +void CapsLockWatcher::Private::watchWayland() { + watchingWayland = true; + auto connection = ConnectionThread::fromApplication(q); + if (!connection) { + qWarning() << "Failed to get connection to Wayland server from QPA"; + return; + } + registry = new Registry{q}; + registry->create(connection); + if (!registry->isValid()) { + qWarning() << "Failed to create valid KWayland registry"; + return; + } + registry->setup(); + + connect(registry, &Registry::seatAnnounced, q, + [this](quint32 name, quint32 version) { + registry_seatAnnounced(name, version); + }); +} + +void CapsLockWatcher::Private::registry_seatAnnounced(quint32 name, + quint32 version) { + Q_ASSERT(registry); + seat = registry->createSeat(name, version, q); + if (!seat->isValid()) { + qWarning() << "Failed to create valid KWayland seat"; + return; + } + + connect(seat, &Seat::hasKeyboardChanged, q, + [this](bool hasKeyboard) { seat_hasKeyboardChanged(hasKeyboard); }); +} + +void CapsLockWatcher::Private::seat_hasKeyboardChanged(bool hasKeyboard) { + Q_ASSERT(seat); + + if (!hasKeyboard) { + qDebug() << "Seat has no keyboard"; + return; + } + + auto keyboard = seat->createKeyboard(q); + if (!keyboard->isValid()) { + qWarning() << "Failed to create valid KWayland keyboard"; + return; + } + + connect(keyboard, &Keyboard::modifiersChanged, q, + [this](quint32, quint32, quint32 locked, quint32) { + keyboard_modifiersChanged(locked); + }); +} + +void CapsLockWatcher::Private::keyboard_modifiersChanged(quint32 locked) { + const bool capsLockIsLocked = (locked & 2u) != 0; + qDebug() << "Caps Lock is locked:" << capsLockIsLocked; + Q_EMIT q->stateChanged(capsLockIsLocked); +} +#endif diff --git a/src/pinentry/capslock/capslock_win.cpp b/src/pinentry/capslock/capslock_win.cpp new file mode 100644 index 00000000..46bc7043 --- /dev/null +++ b/src/pinentry/capslock/capslock_win.cpp @@ -0,0 +1,26 @@ +/* capslock_win.cpp - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ +#include <windows.h> + +#include "capslock.h" + +LockState capsLockState() { + return (GetKeyState(VK_CAPITAL) & 1) ? LockState::On : LockState::Off; +} diff --git a/src/pinentry/focusframe.cpp b/src/pinentry/focusframe.cpp new file mode 100644 index 00000000..492bd4ea --- /dev/null +++ b/src/pinentry/focusframe.cpp @@ -0,0 +1,72 @@ +/* focusframe.cpp - A focus indicator for labels. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "focusframe.h" + +#if QT_CONFIG(graphicseffect) +#include <QGraphicsEffect> +#endif +#include <QStyleOptionFocusRect> +#include <QStylePainter> + +static QRect effectiveWidgetRect(const QWidget *w) +{ + // based on QWidgetPrivate::effectiveRectFor +#if QT_CONFIG(graphicseffect) + const auto* const graphicsEffect = w->graphicsEffect(); + if (graphicsEffect && graphicsEffect->isEnabled()) + return graphicsEffect->boundingRectFor(w->rect()).toAlignedRect(); +#endif // QT_CONFIG(graphicseffect) + return w->rect(); +} + +static QRect clipRect(const QWidget *w) +{ + // based on QWidgetPrivate::clipRect + if (!w->isVisible()) { + return QRect(); + } + QRect r = effectiveWidgetRect(w); + int ox = 0; + int oy = 0; + while (w && w->isVisible() && !w->isWindow() && w->parentWidget()) { + ox -= w->x(); + oy -= w->y(); + w = w->parentWidget(); + r &= QRect(ox, oy, w->width(), w->height()); + } + return r; +} + +void FocusFrame::paintEvent(QPaintEvent *) +{ + if (!widget()) { + return; + } + + QStylePainter p{this}; + QStyleOptionFocusRect option; + initStyleOption(&option); + const int vmargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &option); + const int hmargin = style()->pixelMetric(QStyle::PM_FocusFrameHMargin, &option); + const QRect rect = clipRect(widget()).adjusted(0, 0, hmargin*2, vmargin*2); + p.setClipRect(rect); + p.drawPrimitive(QStyle::PE_FrameFocusRect, option); +} diff --git a/src/pinentry/focusframe.h b/src/pinentry/focusframe.h new file mode 100644 index 00000000..3d2231ea --- /dev/null +++ b/src/pinentry/focusframe.h @@ -0,0 +1,36 @@ +/* focusframe.h - A focus indicator for labels. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __FOCUSFRAME_H__ +#define __FOCUSFRAME_H__ + +#include <QFocusFrame> + +class FocusFrame : public QFocusFrame +{ + Q_OBJECT +public: + using QFocusFrame::QFocusFrame; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +#endif // __FOCUSFRAME_H__ diff --git a/src/pinentry/keyboardfocusindication.cpp b/src/pinentry/keyboardfocusindication.cpp new file mode 100644 index 00000000..d783d981 --- /dev/null +++ b/src/pinentry/keyboardfocusindication.cpp @@ -0,0 +1,43 @@ +/* keyboardfocusindication.cpp - Helper for extended keyboard focus indication. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "keyboardfocusindication.h" + +#include "focusframe.h" + +#include <QApplication> + +KeyboardFocusIndication::KeyboardFocusIndication(QObject *parent) + : QObject{parent} +{ + connect(qApp, &QApplication::focusChanged, this, &KeyboardFocusIndication::updateFocusFrame); +} + +void KeyboardFocusIndication::updateFocusFrame(QWidget *, QWidget *focusWidget) +{ + if (focusWidget && focusWidget->inherits("QLabel") && focusWidget->window()->testAttribute(Qt::WA_KeyboardFocusChange)) { + if (!focusFrame) { + focusFrame = new FocusFrame{focusWidget}; + } + focusFrame->setWidget(focusWidget); + } else if (focusFrame) { + focusFrame->setWidget(nullptr); + } +} diff --git a/src/pinentry/keyboardfocusindication.h b/src/pinentry/keyboardfocusindication.h new file mode 100644 index 00000000..e86a317e --- /dev/null +++ b/src/pinentry/keyboardfocusindication.h @@ -0,0 +1,42 @@ +/* keyboardfocusindication.h - Helper for extended keyboard focus indication. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __KEYBOARDFOCUSINDICATION_H__ +#define __KEYBOARDFOCUSINDICATION_H__ + +#include <QObject> +#include <QPointer> +#include <QWidget> + +class FocusFrame; + +class KeyboardFocusIndication : public QObject +{ + Q_OBJECT +public: + KeyboardFocusIndication(QObject *parent); + +private: + void updateFocusFrame(QWidget *, QWidget *); + + QPointer<FocusFrame> focusFrame; +}; + +#endif // __KEYBOARDFOCUSINDICATION_H__ diff --git a/src/pinentry/password_cache.cpp b/src/pinentry/password_cache.cpp new file mode 100644 index 00000000..b52602d7 --- /dev/null +++ b/src/pinentry/password_cache.cpp @@ -0,0 +1,153 @@ +/* password-cache.c - Password cache support. + Copyright (C) 2015 g10 Code GmbH + + This file is part of PINENTRY. + + PINENTRY 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 2 of the License, or + (at your option) any later version. + + PINENTRY 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 this program; if not, see <https://www.gnu.org/licenses/>. + SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#ifdef HAVE_LIBSECRET +#include <libsecret/secret.h> +#endif + +#include "password_cache.h" +#include "secmem.h" + +#ifdef HAVE_LIBSECRET +static const SecretSchema *gpg_schema(void) { + static const SecretSchema the_schema = { + "org.gnupg.Passphrase", + SECRET_SCHEMA_NONE, + { + {"stored-by", SECRET_SCHEMA_ATTRIBUTE_STRING}, + {"keygrip", SECRET_SCHEMA_ATTRIBUTE_STRING}, + {"NULL", 0}, + }}; + return &the_schema; +} + +static char *keygrip_to_label(const char *keygrip) { + char const prefix[] = "GnuPG: "; + char *label; + + label = malloc(sizeof(prefix) + strlen(keygrip)); + if (label) { + memcpy(label, prefix, sizeof(prefix) - 1); + strcpy(&label[sizeof(prefix) - 1], keygrip); + } + return label; +} +#endif + +void password_cache_save(const char *keygrip, const char *password) { +#ifdef HAVE_LIBSECRET + char *label; + GError *error = NULL; + + if (!*keygrip) return; + + label = keygrip_to_label(keygrip); + if (!label) return; + + if (!secret_password_store_sync(gpg_schema(), SECRET_COLLECTION_DEFAULT, + label, password, NULL, &error, "stored-by", + "GnuPG Pinentry", "keygrip", keygrip, NULL)) { + fprintf(stderr, + "Failed to cache password for key %s with secret service: %s\n", + keygrip, error->message); + + g_error_free(error); + } + + free(label); +#else + (void)keygrip; + (void)password; + return; +#endif +} + +char *password_cache_lookup(const char *keygrip, int *fatal_error) { +#ifdef HAVE_LIBSECRET + GError *error = NULL; + char *password; + char *password2; + + if (!*keygrip) return NULL; + + password = secret_password_lookup_nonpageable_sync(gpg_schema(), NULL, &error, + "keygrip", keygrip, NULL); + + if (error != NULL) { + if (fatal_error) *fatal_error = 1; + + fprintf(stderr, + "Failed to lookup password for key %s with secret service: %s\n", + keygrip, error->message); + g_error_free(error); + return NULL; + } + if (!password) + /* The password for this key is not cached. Just return NULL. */ + return NULL; + + /* The password needs to be returned in secmem allocated memory. */ + password2 = secmem_malloc(strlen(password) + 1); + if (password2) + strcpy(password2, password); + else + fprintf(stderr, "secmem_malloc failed: can't copy password!\n"); + + secret_password_free(password); + + return password2; +#else + (void)keygrip; + (void)fatal_error; + return NULL; +#endif +} + +/* Try and remove the cached password for key grip. Returns -1 on + error, 0 if the key is not found and 1 if the password was + removed. */ +int password_cache_clear(const char *keygrip) { +#ifdef HAVE_LIBSECRET + GError *error = NULL; + int removed = secret_password_clear_sync(gpg_schema(), NULL, &error, + "keygrip", keygrip, NULL); + if (error != NULL) { + fprintf(stderr, + "Failed to clear password for key %s with secret service: %s\n", + keygrip, error->message); + g_debug("%s", error->message); + g_error_free(error); + return -1; + } + if (removed) return 1; + return 0; +#else + (void)keygrip; + return -1; +#endif +} diff --git a/src/pinentry/password_cache.h b/src/pinentry/password_cache.h new file mode 100644 index 00000000..cdba356d --- /dev/null +++ b/src/pinentry/password_cache.h @@ -0,0 +1,30 @@ +/* password_cache.h - Password cache support interfaces. + Copyright (C) 2015 g10 Code GmbH + + This file is part of PINENTRY. + + PINENTRY 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 2 of the License, or + (at your option) any later version. + + PINENTRY 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 this program; if not, see <https://www.gnu.org/licenses/>. + SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef PASSWORD_CACHE_H +#define PASSWORD_CACHE_H + +void password_cache_save(const char *key_grip, const char *password); + +char *password_cache_lookup(const char *key_grip, int *fatal_error); + +int password_cache_clear(const char *keygrip); + +#endif diff --git a/src/pinentry/pinentry.cpp b/src/pinentry/pinentry.cpp new file mode 100644 index 00000000..918944b9 --- /dev/null +++ b/src/pinentry/pinentry.cpp @@ -0,0 +1,1696 @@ +/* pinentry.c - The PIN entry support library + * Copyright (C) 2002, 2003, 2007, 2008, 2010, 2015, 2016, 2021 g10 Code GmbH + * + * This file is part of PINENTRY. + * + * PINENTRY 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 2 of the License, or + * (at your option) any later version. + * + * PINENTRY 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#ifndef WINDOWS +#include <errno.h> +#endif +#include <assert.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> +#ifndef WINDOWS +#include <sys/utsname.h> +#endif +#ifndef WINDOWS +#include <locale.h> +#endif +#include <limits.h> +#ifdef WINDOWS +#include <windows.h> +#endif + +#include <assuan.h> + +#include "argparse.h" +#include "password_cache.h" +#include "pinentry.h" +#include "secmem_util.h" + +#ifdef WINDOWS +#define getpid() GetCurrentProcessId() +#endif + +/* Keep the name of our program here. */ +static char this_pgmname[50]; + +struct pinentry pinentry; + +static const char *flavor_flag; + +/* Because gtk_init removes the --display arg from the command lines + * and our command line parser is called after gtk_init (so that it + * does not see gtk specific options) we don't have a way to get hold + * of the --display option. Our solution is to remember --display in + * the call to pinentry_have_display and set it then in our + * parser. */ +static char *remember_display; + +static void pinentry_reset(int use_defaults) { + /* GPG Agent sets these options once when it starts the pinentry. + Don't reset them. */ + int grab = pinentry.grab; + char *ttyname = pinentry.ttyname; + char *ttytype = pinentry.ttytype_l; + char *ttyalert = pinentry.ttyalert; + char *lc_ctype = pinentry.lc_ctype; + char *lc_messages = pinentry.lc_messages; + int allow_external_password_cache = pinentry.allow_external_password_cache; + char *default_ok = pinentry.default_ok; + char *default_cancel = pinentry.default_cancel; + char *default_prompt = pinentry.default_prompt; + char *default_pwmngr = pinentry.default_pwmngr; + char *default_cf_visi = pinentry.default_cf_visi; + char *default_tt_visi = pinentry.default_tt_visi; + char *default_tt_hide = pinentry.default_tt_hide; + char *default_capshint = pinentry.default_capshint; + char *touch_file = pinentry.touch_file; + unsigned long owner_pid = pinentry.owner_pid; + int owner_uid = pinentry.owner_uid; + char *owner_host = pinentry.owner_host; + int constraints_enforce = pinentry.constraints_enforce; + char *constraints_hint_short = pinentry.constraints_hint_short; + char *constraints_hint_long = pinentry.constraints_hint_long; + char *constraints_error_title = pinentry.constraints_error_title; + + /* These options are set from the command line. Don't reset + them. */ + int debug = pinentry.debug; + char *display = pinentry.display; + int parent_wid = pinentry.parent_wid; + + pinentry_color_t color_fg = pinentry.color_fg; + int color_fg_bright = pinentry.color_fg_bright; + pinentry_color_t color_bg = pinentry.color_bg; + pinentry_color_t color_so = pinentry.color_so; + int color_so_bright = pinentry.color_so_bright; + pinentry_color_t color_ok = pinentry.color_ok; + int color_ok_bright = pinentry.color_ok_bright; + pinentry_color_t color_qualitybar = pinentry.color_qualitybar; + int color_qualitybar_bright = pinentry.color_qualitybar_bright; + + int timeout = pinentry.timeout; + + char *invisible_char = pinentry.invisible_char; + + /* Free any allocated memory. */ + if (use_defaults) { + free(pinentry.ttyname); + free(pinentry.ttytype_l); + free(pinentry.ttyalert); + free(pinentry.lc_ctype); + free(pinentry.lc_messages); + free(pinentry.default_ok); + free(pinentry.default_cancel); + free(pinentry.default_prompt); + free(pinentry.default_pwmngr); + free(pinentry.default_cf_visi); + free(pinentry.default_tt_visi); + free(pinentry.default_tt_hide); + free(pinentry.default_capshint); + free(pinentry.touch_file); + free(pinentry.owner_host); + free(pinentry.display); + free(pinentry.constraints_hint_short); + free(pinentry.constraints_hint_long); + free(pinentry.constraints_error_title); + } + + free(pinentry.title); + free(pinentry.description); + free(pinentry.error); + free(pinentry.prompt); + free(pinentry.ok); + free(pinentry.notok); + free(pinentry.cancel); + secmem_free(pinentry.pin); + free(pinentry.repeat_passphrase); + free(pinentry.repeat_error_string); + free(pinentry.quality_bar); + free(pinentry.quality_bar_tt); + free(pinentry.formatted_passphrase_hint); + free(pinentry.keyinfo); + free(pinentry.specific_err_info); + + /* Reset the pinentry structure. */ + memset(&pinentry, 0, sizeof(pinentry)); + + /* Restore options without a default we want to preserve. */ + pinentry.invisible_char = invisible_char; + + /* Restore other options or set defaults. */ + + if (use_defaults) { + /* Pinentry timeout in seconds. */ + pinentry.timeout = 60; + + /* Global grab. */ + pinentry.grab = 1; + + pinentry.color_fg = PINENTRY_COLOR_DEFAULT; + pinentry.color_fg_bright = 0; + pinentry.color_bg = PINENTRY_COLOR_DEFAULT; + pinentry.color_so = PINENTRY_COLOR_DEFAULT; + pinentry.color_so_bright = 0; + pinentry.color_ok = PINENTRY_COLOR_DEFAULT; + pinentry.color_ok_bright = 0; + pinentry.color_qualitybar = PINENTRY_COLOR_DEFAULT; + pinentry.color_qualitybar_bright = 0; + + pinentry.owner_uid = -1; + } else /* Restore the options. */ + { + pinentry.grab = grab; + pinentry.ttyname = ttyname; + pinentry.ttytype_l = ttytype; + pinentry.ttyalert = ttyalert; + pinentry.lc_ctype = lc_ctype; + pinentry.lc_messages = lc_messages; + pinentry.allow_external_password_cache = allow_external_password_cache; + pinentry.default_ok = default_ok; + pinentry.default_cancel = default_cancel; + pinentry.default_prompt = default_prompt; + pinentry.default_pwmngr = default_pwmngr; + pinentry.default_cf_visi = default_cf_visi; + pinentry.default_tt_visi = default_tt_visi; + pinentry.default_tt_hide = default_tt_hide; + pinentry.default_capshint = default_capshint; + pinentry.touch_file = touch_file; + pinentry.owner_pid = owner_pid; + pinentry.owner_uid = owner_uid; + pinentry.owner_host = owner_host; + pinentry.constraints_enforce = constraints_enforce; + pinentry.constraints_hint_short = constraints_hint_short; + pinentry.constraints_hint_long = constraints_hint_long; + pinentry.constraints_error_title = constraints_error_title; + + pinentry.debug = debug; + pinentry.display = display; + pinentry.parent_wid = parent_wid; + + pinentry.color_fg = color_fg; + pinentry.color_fg_bright = color_fg_bright; + pinentry.color_bg = color_bg; + pinentry.color_so = color_so; + pinentry.color_so_bright = color_so_bright; + pinentry.color_ok = color_ok; + pinentry.color_ok_bright = color_ok_bright; + pinentry.color_qualitybar = color_qualitybar; + pinentry.color_qualitybar_bright = color_qualitybar_bright; + + pinentry.timeout = timeout; + } +} + +static gpg_error_t pinentry_assuan_reset_handler(assuan_context_t ctx, + char *line) { + (void)ctx; + (void)line; + + pinentry_reset(0); + + return 0; +} + +/* Copy TEXT or TEXTLEN to BUFFER and escape as required. Return a + pointer to the end of the new buffer. Note that BUFFER must be + large enough to keep the entire text; allocataing it 3 times of + TEXTLEN is sufficient. */ +static char *copy_and_escape(char *buffer, const void *text, size_t textlen) { + int i; + const unsigned char *s = (unsigned char *)text; + char *p = buffer; + + for (i = 0; i < textlen; i++) { + if (s[i] < ' ' || s[i] == '+') { + snprintf(p, 4, "%%%02X", s[i]); + p += 3; + } else if (s[i] == ' ') + *p++ = '+'; + else + *p++ = s[i]; + } + return p; +} + +/* Perform percent unescaping in STRING and return the new valid length + of the string. A terminating Nul character is inserted at the end of + the unescaped string. + */ +static size_t do_unescape_inplace(char *s) { + unsigned char *p, *p0; + + p = p0 = (unsigned char *)s; + while (*s) { + if (*s == '%' && s[1] && s[2]) { + s++; + *p++ = xtoi_2(s); + s += 2; + } else + *p++ = *s++; + } + *p = 0; + + return (p - p0); +} + +/* Return a malloced copy of the commandline for PID. If this is not + * possible NULL is returned. */ +#ifndef WINDOWS +static char *get_cmdline(unsigned long pid) { + char buffer[200]; + FILE *fp; + size_t i, n; + + snprintf(buffer, sizeof buffer, "/proc/%lu/cmdline", pid); + + fp = fopen(buffer, "rb"); + if (!fp) return NULL; + n = fread(buffer, 1, sizeof buffer - 1, fp); + if (n < sizeof buffer - 1 && ferror(fp)) { + /* Some error occurred. */ + fclose(fp); + return NULL; + } + fclose(fp); + if (n == 0) return NULL; + /* Arguments are delimited by Nuls. We should do proper quoting but + * that can be a bit complicated, thus we simply replace the Nuls by + * spaces. */ + for (i = 0; i < n; i++) + if (!buffer[i] && i < n - 1) buffer[i] = ' '; + buffer[i] = 0; /* Make sure the last byte is the string terminator. */ + + return strdup(buffer); +} +#endif /*!WINDOWS*/ + +/* Atomically ask the kernel for information about process PID. + * Return a malloc'ed copy of the process name as long as the process + * uid matches UID. If it cannot determine that the process has uid + * UID, it returns NULL. + * + * This is not as informative as get_cmdline, but it verifies that the + * process does belong to the user in question. + */ +#ifndef WINDOWS +static char *get_pid_name_for_uid(unsigned long pid, int uid) { + char buffer[400]; + FILE *fp; + size_t end, n; + char *uidstr; + + snprintf(buffer, sizeof buffer, "/proc/%lu/status", pid); + + fp = fopen(buffer, "rb"); + if (!fp) return NULL; + n = fread(buffer, 1, sizeof buffer - 1, fp); + if (n < sizeof buffer - 1 && ferror(fp)) { + /* Some error occurred. */ + fclose(fp); + return NULL; + } + fclose(fp); + if (n == 0) return NULL; + buffer[n] = 0; + /* Fixme: Is it specified that "Name" is always the first line? For + * robustness I would prefer to have a real parser here. -wk */ + if (strncmp(buffer, "Name:\t", 6)) return NULL; + end = strcspn(buffer + 6, "\n") + 6; + buffer[end] = 0; + + /* check that uid matches what we expect */ + uidstr = strstr(buffer + end + 1, "\nUid:\t"); + if (!uidstr) return NULL; + if (atoi(uidstr + 6) != uid) return NULL; + + return strdup(buffer + 6); +} +#endif /*!WINDOWS*/ + +const char *pinentry_get_pgmname(void) { return this_pgmname; } + +/* Return a malloced string with the title. The caller mus free the + * string. If no title is available or the title string has an error + * NULL is returned. */ +char *pinentry_get_title(pinentry_t pe) { + char *title; + + if (pe->title) title = strdup(pe->title); +#ifndef WINDOWS + else if (pe->owner_pid) { + char buf[200]; + struct utsname utsbuf; + char *pidname = NULL; + char *cmdline = NULL; + + if (pe->owner_host && !uname(&utsbuf) && + !strcmp(utsbuf.nodename, pe->owner_host)) { + pidname = get_pid_name_for_uid(pe->owner_pid, pe->owner_uid); + if (pidname) cmdline = get_cmdline(pe->owner_pid); + } + + if (pe->owner_host && (cmdline || pidname)) + snprintf(buf, sizeof buf, "[%lu]@%s (%s)", pe->owner_pid, pe->owner_host, + cmdline ? cmdline : pidname); + else if (pe->owner_host) + snprintf(buf, sizeof buf, "[%lu]@%s", pe->owner_pid, pe->owner_host); + else + snprintf(buf, sizeof buf, "[%lu] <unknown host>", pe->owner_pid); + free(pidname); + free(cmdline); + title = strdup(buf); + } +#endif /*!WINDOWS*/ + else + title = strdup(this_pgmname); + + return title; +} + +/* Run a quality inquiry for PASSPHRASE of LENGTH. (We need LENGTH + because not all backends might be able to return a proper + C-string.). Returns: A value between -100 and 100 to give an + estimate of the passphrase's quality. Negative values are use if + the caller won't even accept that passphrase. Note that we expect + just one data line which should not be escaped in any represent a + numeric signed decimal value. Extra data is currently ignored but + should not be send at all. */ +int pinentry_inq_quality(pinentry_t pin, const char *passphrase, + size_t length) { + assuan_context_t ctx = (assuan_context_t)pin->ctx_assuan; + const char prefix[] = "INQUIRE QUALITY "; + char *command; + char *line; + size_t linelen; + int gotvalue = 0; + int value = 0; + int rc; + + if (!ctx) return 0; /* Can't run the callback. */ + + if (length > 300) + length = 300; /* Limit so that it definitely fits into an Assuan + line. */ + + command = (char *)secmem_malloc(strlen(prefix) + 3 * length + 1); + if (!command) return 0; + strcpy(command, prefix); + copy_and_escape(command + strlen(command), passphrase, length); + rc = assuan_write_line(ctx, command); + secmem_free(command); + if (rc) { + fprintf(stderr, "ASSUAN WRITE LINE failed: rc=%d\n", rc); + return 0; + } + + for (;;) { + do { + rc = assuan_read_line(ctx, &line, &linelen); + if (rc) { + fprintf(stderr, "ASSUAN READ LINE failed: rc=%d\n", rc); + return 0; + } + } while (*line == '#' || !linelen); + if (line[0] == 'E' && line[1] == 'N' && line[2] == 'D' && + (!line[3] || line[3] == ' ')) + break; /* END command received*/ + if (line[0] == 'C' && line[1] == 'A' && line[2] == 'N' && + (!line[3] || line[3] == ' ')) + break; /* CAN command received*/ + if (line[0] == 'E' && line[1] == 'R' && line[2] == 'R' && + (!line[3] || line[3] == ' ')) + break; /* ERR command received*/ + if (line[0] != 'D' || line[1] != ' ' || linelen < 3 || gotvalue) continue; + gotvalue = 1; + value = atoi(line + 2); + } + if (value < -100) + value = -100; + else if (value > 100) + value = 100; + + return value; +} + +/* Run a checkpin inquiry */ +char *pinentry_inq_checkpin(pinentry_t pin, const char *passphrase, + size_t length) { + assuan_context_t ctx = (assuan_context_t)pin->ctx_assuan; + const char prefix[] = "INQUIRE CHECKPIN "; + char *command; + char *line; + size_t linelen; + int gotvalue = 0; + char *value = NULL; + int rc; + + if (!ctx) return 0; /* Can't run the callback. */ + + if (length > 300) + length = 300; /* Limit so that it definitely fits into an Assuan + line. */ + + command = (char *)secmem_malloc(strlen(prefix) + 3 * length + 1); + if (!command) return 0; + strcpy(command, prefix); + copy_and_escape(command + strlen(command), passphrase, length); + rc = assuan_write_line(ctx, command); + secmem_free(command); + if (rc) { + fprintf(stderr, "ASSUAN WRITE LINE failed: rc=%d\n", rc); + return 0; + } + + for (;;) { + do { + rc = assuan_read_line(ctx, &line, &linelen); + if (rc) { + fprintf(stderr, "ASSUAN READ LINE failed: rc=%d\n", rc); + return 0; + } + } while (*line == '#' || !linelen); + if (line[0] == 'E' && line[1] == 'N' && line[2] == 'D' && + (!line[3] || line[3] == ' ')) + break; /* END command received*/ + if (line[0] == 'C' && line[1] == 'A' && line[2] == 'N' && + (!line[3] || line[3] == ' ')) + break; /* CAN command received*/ + if (line[0] == 'E' && line[1] == 'R' && line[2] == 'R' && + (!line[3] || line[3] == ' ')) + break; /* ERR command received*/ + if (line[0] != 'D' || line[1] != ' ' || linelen < 3 || gotvalue) continue; + gotvalue = 1; + value = strdup(line + 2); + } + + return value; +} + +/* Run a genpin inquiry */ +char *pinentry_inq_genpin(pinentry_t pin) { + assuan_context_t ctx = (assuan_context_t)pin->ctx_assuan; + const char prefix[] = "INQUIRE GENPIN"; + char *line; + size_t linelen; + int gotvalue = 0; + char *value = NULL; + int rc; + + if (!ctx) return 0; /* Can't run the callback. */ + + rc = assuan_write_line(ctx, prefix); + if (rc) { + fprintf(stderr, "ASSUAN WRITE LINE failed: rc=%d\n", rc); + return 0; + } + + for (;;) { + do { + rc = assuan_read_line(ctx, &line, &linelen); + if (rc) { + fprintf(stderr, "ASSUAN READ LINE failed: rc=%d\n", rc); + free(value); + return 0; + } + } while (*line == '#' || !linelen); + if (line[0] == 'E' && line[1] == 'N' && line[2] == 'D' && + (!line[3] || line[3] == ' ')) + break; /* END command received*/ + if (line[0] == 'C' && line[1] == 'A' && line[2] == 'N' && + (!line[3] || line[3] == ' ')) + break; /* CAN command received*/ + if (line[0] == 'E' && line[1] == 'R' && line[2] == 'R' && + (!line[3] || line[3] == ' ')) + break; /* ERR command received*/ + if (line[0] != 'D' || line[1] != ' ' || linelen < 3 || gotvalue) continue; + gotvalue = 1; + value = strdup(line + 2); + } + + return value; +} + +/* Try to make room for at least LEN bytes in the pinentry. Returns + new buffer on success and 0 on failure or when the old buffer is + sufficient. */ +char *pinentry_setbufferlen(pinentry_t pin, int len) { + char *newp; + + if (pin->pin_len) + assert(pin->pin); + else + assert(!pin->pin); + + if (len < 2048) len = 2048; + + if (len <= pin->pin_len) return pin->pin; + + newp = (char *)secmem_realloc(pin->pin, len); + if (newp) { + pin->pin = newp; + pin->pin_len = len; + } else { + secmem_free(pin->pin); + pin->pin = 0; + pin->pin_len = 0; + } + return newp; +} + +static void pinentry_setbuffer_clear(pinentry_t pin) { + if (!pin->pin) { + assert(pin->pin_len == 0); + return; + } + + assert(pin->pin_len > 0); + + secmem_free(pin->pin); + pin->pin = NULL; + pin->pin_len = 0; +} + +static void pinentry_setbuffer_init(pinentry_t pin) { + pinentry_setbuffer_clear(pin); + pinentry_setbufferlen(pin, 0); +} + +/* passphrase better be alloced with secmem_alloc. */ +void pinentry_setbuffer_use(pinentry_t pin, char *passphrase, int len) { + if (!passphrase) { + assert(len == 0); + pinentry_setbuffer_clear(pin); + + return; + } + + if (passphrase && len == 0) len = strlen(passphrase) + 1; + + if (pin->pin) secmem_free(pin->pin); + + pin->pin = passphrase; + pin->pin_len = len; +} + +static struct assuan_malloc_hooks assuan_malloc_hooks = { + secmem_malloc, secmem_realloc, secmem_free}; + +/* Initialize the secure memory subsystem, drop privileges and return. + Must be called early. */ +void pinentry_init(const char *pgmname) { + /* Store away our name. */ + if (strlen(pgmname) > sizeof this_pgmname - 2) abort(); + strcpy(this_pgmname, pgmname); + + gpgrt_check_version(NULL); + + /* Initialize secure memory. 1 is too small, so the default size + will be used. */ + secmem_init(1); + secmem_set_flags(SECMEM_WARN); + drop_privs(); + + if (atexit(secmem_term)) { + /* FIXME: Could not register at-exit function, bail out. */ + } + + assuan_set_malloc_hooks(&assuan_malloc_hooks); +} + +/* Simple test to check whether DISPLAY is set or the option --display + was given. Used to decide whether the GUI or curses should be + initialized. */ +int pinentry_have_display(int argc, char **argv) { + int found = 0; + + for (; argc; argc--, argv++) { + if (!strcmp(*argv, "--display")) { + if (argv[1] && !remember_display) { + remember_display = strdup(argv[1]); + if (!remember_display) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + } + found = 1; + break; + } else if (!strncmp(*argv, "--display=", 10)) { + if (!remember_display) { + remember_display = strdup(*argv + 10); + if (!remember_display) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + } + found = 1; + break; + } + } + +#ifndef WINDOWS + { + const char *s; + s = getenv("DISPLAY"); + if (s && *s) found = 1; + } +#endif + + return found; +} + +/* Print usage information and and provide strings for help. */ +static const char *my_strusage(int level) { + const char *p; + + switch (level) { + case 11: + p = this_pgmname; + break; + case 12: + p = "pinentry"; + break; + case 13: + p = 0; + break; + case 14: + p = "Copyright (C) 2023 Saturneric"; + break; + case 19: + p = "Please report bugs to <[email protected]>.\n"; + break; + case 1: + case 40: { + static char *str; + + if (!str) { + size_t n = 50 + strlen(this_pgmname); + str = static_cast<char *>(malloc(n)); + if (str) { + snprintf(str, n, "Usage: %s [options] (-h for help)", this_pgmname); + } + } + p = str; + } break; + case 41: + p = "Ask securely for a secret and print it to stdout."; + break; + + case 42: + p = "1"; /* Flag print 40 as part of 41. */ + break; + + default: + p = NULL; + break; + } + return p; +} + +char *parse_color(char *arg, pinentry_color_t *color_p, int *bright_p) { + static struct { + const char *name; + pinentry_color_t color; + } colors[] = { + {"none", PINENTRY_COLOR_NONE}, {"default", PINENTRY_COLOR_DEFAULT}, + {"black", PINENTRY_COLOR_BLACK}, {"red", PINENTRY_COLOR_RED}, + {"green", PINENTRY_COLOR_GREEN}, {"yellow", PINENTRY_COLOR_YELLOW}, + {"blue", PINENTRY_COLOR_BLUE}, {"magenta", PINENTRY_COLOR_MAGENTA}, + {"cyan", PINENTRY_COLOR_CYAN}, {"white", PINENTRY_COLOR_WHITE}}; + + int i; + char *new_arg; + pinentry_color_t color = PINENTRY_COLOR_DEFAULT; + + if (!arg) return NULL; + + new_arg = strchr(arg, ','); + if (new_arg) new_arg++; + + if (bright_p) { + const char *bname[] = {"bright-", "bright", "bold-", "bold"}; + + *bright_p = 0; + for (i = 0; i < sizeof(bname) / sizeof(bname[0]); i++) + if (!strncasecmp(arg, bname[i], strlen(bname[i]))) { + *bright_p = 1; + arg += strlen(bname[i]); + } + } + + for (i = 0; i < sizeof(colors) / sizeof(colors[0]); i++) + if (!strncasecmp(arg, colors[i].name, strlen(colors[i].name))) + color = colors[i].color; + + *color_p = color; + return new_arg; +} + +/* Parse the command line options. May exit the program if only help + or version output is requested. */ +void pinentry_parse_opts(int argc, char *argv[]) { + static ARGPARSE_OPTS opts[] = { + ARGPARSE_s_n('d', "debug", "Turn on debugging output"), + ARGPARSE_s_s('D', "display", "|DISPLAY|Set the X display"), + ARGPARSE_s_s('T', "ttyname", "|FILE|Set the tty terminal node name"), + ARGPARSE_s_s('N', "ttytype", "|NAME|Set the tty terminal type"), + ARGPARSE_s_s('C', "lc-ctype", "|STRING|Set the tty LC_CTYPE value"), + ARGPARSE_s_s('M', "lc-messages", "|STRING|Set the tty LC_MESSAGES value"), + ARGPARSE_s_i('o', "timeout", + "|SECS|Timeout waiting for input after this many seconds"), + ARGPARSE_s_n('g', "no-global-grab", + "Grab keyboard only while window is focused"), + ARGPARSE_s_u('W', "parent-wid", "Parent window ID (for positioning)"), + ARGPARSE_s_s('c', "colors", "|STRING|Set custom colors for ncurses"), + ARGPARSE_s_s('a', "ttyalert", + "|STRING|Set the alert mode (none, beep or flash)"), + ARGPARSE_end()}; + ARGPARSE_ARGS pargs = {&argc, &argv, 0}; + + set_strusage(my_strusage); + + pinentry_reset(1); + + while (arg_parse(&pargs, opts)) { + switch (pargs.r_opt) { + case 'd': + pinentry.debug = 1; + break; + case 'g': + pinentry.grab = 0; + break; + + case 'D': + /* Note, this is currently not used because the GUI engine + has already been initialized when parsing these options. */ + pinentry.display = strdup(pargs.r.ret_str); + if (!pinentry.display) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + break; + case 'T': + pinentry.ttyname = strdup(pargs.r.ret_str); + if (!pinentry.ttyname) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + break; + case 'N': + pinentry.ttytype_l = strdup(pargs.r.ret_str); + if (!pinentry.ttytype_l) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + break; + case 'C': + pinentry.lc_ctype = strdup(pargs.r.ret_str); + if (!pinentry.lc_ctype) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + break; + case 'M': + pinentry.lc_messages = strdup(pargs.r.ret_str); + if (!pinentry.lc_messages) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + break; + case 'W': + pinentry.parent_wid = pargs.r.ret_ulong; + break; + + case 'c': { + char *tmpstr = pargs.r.ret_str; + + tmpstr = + parse_color(tmpstr, &pinentry.color_fg, &pinentry.color_fg_bright); + tmpstr = parse_color(tmpstr, &pinentry.color_bg, NULL); + tmpstr = + parse_color(tmpstr, &pinentry.color_so, &pinentry.color_so_bright); + tmpstr = + parse_color(tmpstr, &pinentry.color_ok, &pinentry.color_ok_bright); + tmpstr = parse_color(tmpstr, &pinentry.color_qualitybar, + &pinentry.color_qualitybar_bright); + } break; + + case 'o': + pinentry.timeout = pargs.r.ret_int; + break; + + case 'a': + pinentry.ttyalert = strdup(pargs.r.ret_str); + if (!pinentry.ttyalert) { +#ifndef WINDOWS + fprintf(stderr, "%s: %s\n", this_pgmname, strerror(errno)); +#endif + exit(EXIT_FAILURE); + } + break; + + default: + pargs.err = ARGPARSE_PRINT_WARNING; + break; + } + } + + if (!pinentry.display && remember_display) { + pinentry.display = remember_display; + remember_display = NULL; + } +} + +/* Set the optional flag used with getinfo. */ +void pinentry_set_flavor_flag(const char *string) { flavor_flag = string; } + +static gpg_error_t option_handler(assuan_context_t ctx, const char *key, + const char *value) { + (void)ctx; + + if (!strcmp(key, "no-grab") && !*value) + pinentry.grab = 0; + else if (!strcmp(key, "grab") && !*value) + pinentry.grab = 1; + else if (!strcmp(key, "debug-wait")) { +#ifndef WINDOWS + fprintf(stderr, "%s: waiting for debugger - my pid is %u ...\n", + this_pgmname, (unsigned int)getpid()); + sleep(*value ? atoi(value) : 5); + fprintf(stderr, "%s: ... okay\n", this_pgmname); +#endif + } else if (!strcmp(key, "display")) { + if (pinentry.display) free(pinentry.display); + pinentry.display = strdup(value); + if (!pinentry.display) return gpg_error_from_syserror(); + } else if (!strcmp(key, "ttyname")) { + if (pinentry.ttyname) free(pinentry.ttyname); + pinentry.ttyname = strdup(value); + if (!pinentry.ttyname) return gpg_error_from_syserror(); + } else if (!strcmp(key, "ttytype")) { + if (pinentry.ttytype_l) free(pinentry.ttytype_l); + pinentry.ttytype_l = strdup(value); + if (!pinentry.ttytype_l) return gpg_error_from_syserror(); + } else if (!strcmp(key, "ttyalert")) { + if (pinentry.ttyalert) free(pinentry.ttyalert); + pinentry.ttyalert = strdup(value); + if (!pinentry.ttyalert) return gpg_error_from_syserror(); + } else if (!strcmp(key, "lc-ctype")) { + if (pinentry.lc_ctype) free(pinentry.lc_ctype); + pinentry.lc_ctype = strdup(value); + if (!pinentry.lc_ctype) return gpg_error_from_syserror(); + } else if (!strcmp(key, "lc-messages")) { + if (pinentry.lc_messages) free(pinentry.lc_messages); + pinentry.lc_messages = strdup(value); + if (!pinentry.lc_messages) return gpg_error_from_syserror(); + } else if (!strcmp(key, "owner")) { + long along; + char *endp; + + free(pinentry.owner_host); + pinentry.owner_host = NULL; + pinentry.owner_uid = -1; + pinentry.owner_pid = 0; + + errno = 0; + along = strtol(value, &endp, 10); + if (along && !errno) { + pinentry.owner_pid = (unsigned long)along; + if (*endp) { + errno = 0; + if (*endp == '/') { /* we have a uid */ + endp++; + along = strtol(endp, &endp, 10); + if (along >= 0 && !errno) pinentry.owner_uid = (int)along; + } + if (endp) { + while (*endp == ' ') endp++; + if (*endp) { + pinentry.owner_host = strdup(endp); + for (endp = pinentry.owner_host; *endp && *endp != ' '; endp++) + ; + *endp = 0; + } + } + } + } + } else if (!strcmp(key, "parent-wid")) { + pinentry.parent_wid = atoi(value); + /* FIXME: Use strtol and add some error handling. */ + } else if (!strcmp(key, "touch-file")) { + if (pinentry.touch_file) free(pinentry.touch_file); + pinentry.touch_file = strdup(value); + if (!pinentry.touch_file) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-ok")) { + pinentry.default_ok = strdup(value); + if (!pinentry.default_ok) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-cancel")) { + pinentry.default_cancel = strdup(value); + if (!pinentry.default_cancel) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-prompt")) { + pinentry.default_prompt = strdup(value); + if (!pinentry.default_prompt) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-pwmngr")) { + pinentry.default_pwmngr = strdup(value); + if (!pinentry.default_pwmngr) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-cf-visi")) { + pinentry.default_cf_visi = strdup(value); + if (!pinentry.default_cf_visi) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-tt-visi")) { + pinentry.default_tt_visi = strdup(value); + if (!pinentry.default_tt_visi) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-tt-hide")) { + pinentry.default_tt_hide = strdup(value); + if (!pinentry.default_tt_hide) return gpg_error_from_syserror(); + } else if (!strcmp(key, "default-capshint")) { + pinentry.default_capshint = strdup(value); + if (!pinentry.default_capshint) return gpg_error_from_syserror(); + } else if (!strcmp(key, "allow-external-password-cache") && !*value) { + pinentry.allow_external_password_cache = 1; + pinentry.tried_password_cache = 0; + } else if (!strcmp(key, "allow-emacs-prompt") && !*value) { +#ifdef INSIDE_EMACS + pinentry_enable_emacs_cmd_handler(); +#endif + } else if (!strcmp(key, "invisible-char")) { + if (pinentry.invisible_char) free(pinentry.invisible_char); + pinentry.invisible_char = strdup(value); + if (!pinentry.invisible_char) return gpg_error_from_syserror(); + } else if (!strcmp(key, "formatted-passphrase") && !*value) { + pinentry.formatted_passphrase = 1; + } else if (!strcmp(key, "formatted-passphrase-hint")) { + if (pinentry.formatted_passphrase_hint) + free(pinentry.formatted_passphrase_hint); + pinentry.formatted_passphrase_hint = strdup(value); + if (!pinentry.formatted_passphrase_hint) return gpg_error_from_syserror(); + do_unescape_inplace(pinentry.formatted_passphrase_hint); + } else if (!strcmp(key, "constraints-enforce") && !*value) + pinentry.constraints_enforce = 1; + else if (!strcmp(key, "constraints-hint-short")) { + if (pinentry.constraints_hint_short) free(pinentry.constraints_hint_short); + pinentry.constraints_hint_short = strdup(value); + if (!pinentry.constraints_hint_short) return gpg_error_from_syserror(); + do_unescape_inplace(pinentry.constraints_hint_short); + } else if (!strcmp(key, "constraints-hint-long")) { + if (pinentry.constraints_hint_long) free(pinentry.constraints_hint_long); + pinentry.constraints_hint_long = strdup(value); + if (!pinentry.constraints_hint_long) return gpg_error_from_syserror(); + do_unescape_inplace(pinentry.constraints_hint_long); + } else if (!strcmp(key, "constraints-error-title")) { + if (pinentry.constraints_error_title) + free(pinentry.constraints_error_title); + pinentry.constraints_error_title = strdup(value); + if (!pinentry.constraints_error_title) return gpg_error_from_syserror(); + do_unescape_inplace(pinentry.constraints_error_title); + } else + return gpg_error(GPG_ERR_UNKNOWN_OPTION); + return 0; +} + +/* Note, that it is sufficient to allocate the target string D as + long as the source string S, i.e.: strlen(s)+1; */ +static void strcpy_escaped(char *d, const char *s) { + while (*s) { + if (*s == '%' && s[1] && s[2]) { + s++; + *d++ = xtoi_2(s); + s += 2; + } else + *d++ = *s++; + } + *d = 0; +} + +static void write_status_error(assuan_context_t ctx, pinentry_t pe) { + char buf[500]; + const char *pgm; + + pgm = strchr(this_pgmname, '-'); + if (pgm && pgm[1]) + pgm++; + else + pgm = this_pgmname; + + snprintf(buf, sizeof buf, "%s.%s %d %s", pgm, + pe->specific_err_loc ? pe->specific_err_loc : "?", pe->specific_err, + pe->specific_err_info ? pe->specific_err_info : ""); + assuan_write_status(ctx, "ERROR", buf); +} + +static gpg_error_t cmd_setdesc(assuan_context_t ctx, char *line) { + char *newd; + + (void)ctx; + + newd = (char *)malloc(strlen(line) + 1); + if (!newd) return gpg_error_from_syserror(); + + strcpy_escaped(newd, line); + if (pinentry.description) free(pinentry.description); + pinentry.description = newd; + return 0; +} + +static gpg_error_t cmd_setprompt(assuan_context_t ctx, char *line) { + char *newp; + + (void)ctx; + + newp = (char *)malloc(strlen(line) + 1); + if (!newp) return gpg_error_from_syserror(); + + strcpy_escaped(newp, line); + if (pinentry.prompt) free(pinentry.prompt); + pinentry.prompt = newp; + return 0; +} + +/* The data provided at LINE may be used by pinentry implementations + to identify a key for caching strategies of its own. The empty + string and --clear mean that the key does not have a stable + identifier. */ +static gpg_error_t cmd_setkeyinfo(assuan_context_t ctx, char *line) { + (void)ctx; + + if (pinentry.keyinfo) free(pinentry.keyinfo); + + if (*line && strcmp(line, "--clear") != 0) + pinentry.keyinfo = strdup(line); + else + pinentry.keyinfo = NULL; + + return 0; +} + +static gpg_error_t cmd_setrepeat(assuan_context_t ctx, char *line) { + char *p; + + (void)ctx; + + p = (char *)malloc(strlen(line) + 1); + if (!p) return gpg_error_from_syserror(); + + strcpy_escaped(p, line); + free(pinentry.repeat_passphrase); + pinentry.repeat_passphrase = p; + return 0; +} + +static gpg_error_t cmd_setrepeatok(assuan_context_t ctx, char *line) { + char *p; + + (void)ctx; + + p = (char *)malloc(strlen(line) + 1); + if (!p) return gpg_error_from_syserror(); + + strcpy_escaped(p, line); + free(pinentry.repeat_ok_string); + pinentry.repeat_ok_string = p; + return 0; +} + +static gpg_error_t cmd_setrepeaterror(assuan_context_t ctx, char *line) { + char *p; + + (void)ctx; + + p = (char *)malloc(strlen(line) + 1); + if (!p) return gpg_error_from_syserror(); + + strcpy_escaped(p, line); + free(pinentry.repeat_error_string); + pinentry.repeat_error_string = p; + return 0; +} + +static gpg_error_t cmd_seterror(assuan_context_t ctx, char *line) { + char *newe; + + (void)ctx; + + newe = (char *)malloc(strlen(line) + 1); + if (!newe) return gpg_error_from_syserror(); + + strcpy_escaped(newe, line); + if (pinentry.error) free(pinentry.error); + pinentry.error = newe; + return 0; +} + +static gpg_error_t cmd_setok(assuan_context_t ctx, char *line) { + char *newo; + + (void)ctx; + + newo = (char *)malloc(strlen(line) + 1); + if (!newo) return gpg_error_from_syserror(); + + strcpy_escaped(newo, line); + if (pinentry.ok) free(pinentry.ok); + pinentry.ok = newo; + return 0; +} + +static gpg_error_t cmd_setnotok(assuan_context_t ctx, char *line) { + char *newo; + + (void)ctx; + + newo = (char *)malloc(strlen(line) + 1); + if (!newo) return gpg_error_from_syserror(); + + strcpy_escaped(newo, line); + if (pinentry.notok) free(pinentry.notok); + pinentry.notok = newo; + return 0; +} + +static gpg_error_t cmd_setcancel(assuan_context_t ctx, char *line) { + char *newc; + + (void)ctx; + + newc = (char *)malloc(strlen(line) + 1); + if (!newc) return gpg_error_from_syserror(); + + strcpy_escaped(newc, line); + if (pinentry.cancel) free(pinentry.cancel); + pinentry.cancel = newc; + return 0; +} + +static gpg_error_t cmd_settimeout(assuan_context_t ctx, char *line) { + (void)ctx; + + if (line && *line) pinentry.timeout = atoi(line); + + return 0; +} + +static gpg_error_t cmd_settitle(assuan_context_t ctx, char *line) { + char *newt; + + (void)ctx; + + newt = (char *)malloc(strlen(line) + 1); + if (!newt) return gpg_error_from_syserror(); + + strcpy_escaped(newt, line); + if (pinentry.title) free(pinentry.title); + pinentry.title = newt; + return 0; +} + +static gpg_error_t cmd_setqualitybar(assuan_context_t ctx, char *line) { + char *newval; + + (void)ctx; + + if (!*line) line = "Quality:"; + + newval = (char *)malloc(strlen(line) + 1); + if (!newval) return gpg_error_from_syserror(); + + strcpy_escaped(newval, line); + if (pinentry.quality_bar) free(pinentry.quality_bar); + pinentry.quality_bar = newval; + return 0; +} + +/* Set the tooltip to be used for a quality bar. */ +static gpg_error_t cmd_setqualitybar_tt(assuan_context_t ctx, char *line) { + char *newval; + + (void)ctx; + + if (*line) { + newval = (char *)malloc(strlen(line) + 1); + if (!newval) return gpg_error_from_syserror(); + + strcpy_escaped(newval, line); + } else + newval = NULL; + if (pinentry.quality_bar_tt) free(pinentry.quality_bar_tt); + pinentry.quality_bar_tt = newval; + return 0; +} + +/* Set the tooltip to be used for a generate action. */ +static gpg_error_t cmd_setgenpin_tt(assuan_context_t ctx, char *line) { + char *newval; + + (void)ctx; + + if (*line) { + newval = (char *)malloc(strlen(line) + 1); + if (!newval) return gpg_error_from_syserror(); + + strcpy_escaped(newval, line); + } else + newval = NULL; + if (pinentry.genpin_tt) free(pinentry.genpin_tt); + pinentry.genpin_tt = newval; + return 0; +} + +/* Set the label to be used for a generate action. */ +static gpg_error_t cmd_setgenpin_label(assuan_context_t ctx, char *line) { + char *newval; + + (void)ctx; + + if (*line) { + newval = (char *)malloc(strlen(line) + 1); + if (!newval) return gpg_error_from_syserror(); + + strcpy_escaped(newval, line); + } else + newval = NULL; + if (pinentry.genpin_label) free(pinentry.genpin_label); + pinentry.genpin_label = newval; + return 0; +} + +static gpg_error_t cmd_getpin(assuan_context_t ctx, char *line) { + int result; + int set_prompt = 0; + int just_read_password_from_cache = 0; + + (void)line; + + pinentry_setbuffer_init(&pinentry); + if (!pinentry.pin) return gpg_error(GPG_ERR_ENOMEM); + + pinentry.confirm = 0; + + /* Try reading from the password cache. */ + if (/* If repeat passphrase is set, then we don't want to read from + the cache. */ + !pinentry.repeat_passphrase + /* Are we allowed to read from the cache? */ + && pinentry.allow_external_password_cache && + pinentry.keyinfo + /* Only read from the cache if we haven't already tried it. */ + && !pinentry.tried_password_cache + /* If the last read resulted in an error, then don't read from + the cache. */ + && !pinentry.error) { + char *password; + int give_up_on_password_store = 0; + + pinentry.tried_password_cache = 1; + + password = + password_cache_lookup(pinentry.keyinfo, &give_up_on_password_store); + if (give_up_on_password_store) pinentry.allow_external_password_cache = 0; + + if (password) + /* There is a cached password. Try it. */ + { + int len = strlen(password) + 1; + if (len > pinentry.pin_len) len = pinentry.pin_len; + + memcpy(pinentry.pin, password, len); + pinentry.pin[len] = '\0'; + + secmem_free(password); + + pinentry.pin_from_cache = 1; + + assuan_write_status(ctx, "PASSWORD_FROM_CACHE", ""); + + /* Result is the length of the password not including the + NUL terminator. */ + result = len - 1; + + just_read_password_from_cache = 1; + + goto out; + } + } + + /* The password was not cached (or we are not allowed to / cannot + use the cache). Prompt the user. */ + pinentry.pin_from_cache = 0; + + if (!pinentry.prompt) { + pinentry.prompt = const_cast<char *>( + pinentry.default_prompt ? pinentry.default_prompt : "PIN:"); + set_prompt = 1; + } + pinentry.locale_err = 0; + pinentry.specific_err = 0; + pinentry.specific_err_loc = NULL; + free(pinentry.specific_err_info); + pinentry.specific_err_info = NULL; + pinentry.close_button = 0; + pinentry.repeat_okay = 0; + pinentry.one_button = 0; + pinentry.ctx_assuan = ctx; + result = (*pinentry_cmd_handler)(&pinentry); + pinentry.ctx_assuan = NULL; + if (pinentry.error) { + free(pinentry.error); + pinentry.error = NULL; + } + if (pinentry.repeat_passphrase) { + free(pinentry.repeat_passphrase); + pinentry.repeat_passphrase = NULL; + } + if (set_prompt) pinentry.prompt = NULL; + + pinentry.quality_bar = 0; /* Reset it after the command. */ + + if (pinentry.close_button) assuan_write_status(ctx, "BUTTON_INFO", "close"); + + if (result < 0) { + pinentry_setbuffer_clear(&pinentry); + if (pinentry.specific_err) { + write_status_error(ctx, &pinentry); + + if (gpg_err_code(pinentry.specific_err) == GPG_ERR_FULLY_CANCELED) + assuan_set_flag(ctx, ASSUAN_FORCE_CLOSE, 1); + + return pinentry.specific_err; + } + return (pinentry.locale_err ? gpg_error(GPG_ERR_LOCALE_PROBLEM) + : gpg_error(GPG_ERR_CANCELED)); + } + +out: + if (result) { + if (pinentry.repeat_okay) assuan_write_status(ctx, "PIN_REPEATED", ""); + assuan_begin_confidential(ctx); + result = assuan_send_data(ctx, pinentry.pin, strlen(pinentry.pin)); + if (!result) result = assuan_send_data(ctx, NULL, 0); + assuan_end_confidential(ctx); + + if (/* GPG Agent says it's okay. */ + pinentry.allow_external_password_cache && + pinentry.keyinfo + /* We didn't just read it from the cache. */ + && !just_read_password_from_cache + /* And the user said it's okay. */ + && pinentry.may_cache_password) + /* Cache the password. */ + password_cache_save(pinentry.keyinfo, pinentry.pin); + } + + pinentry_setbuffer_clear(&pinentry); + + return result; +} + +/* Note that the option --one-button is a hack to allow the use of old + pinentries while the caller is ignoring the result. Given that + options have never been used or flagged as an error the new option + is an easy way to enable the messsage mode while not requiring to + update pinentry or to have the caller test for the message + command. New applications which are free to require an updated + pinentry should use MESSAGE instead. */ +static gpg_error_t cmd_confirm(assuan_context_t ctx, char *line) { + int result; + + pinentry.one_button = !!strstr(line, "--one-button"); + pinentry.quality_bar = 0; + pinentry.close_button = 0; + pinentry.locale_err = 0; + pinentry.specific_err = 0; + pinentry.specific_err_loc = NULL; + free(pinentry.specific_err_info); + pinentry.specific_err_info = NULL; + pinentry.canceled = 0; + pinentry.confirm = 1; + pinentry_setbuffer_clear(&pinentry); + result = (*pinentry_cmd_handler)(&pinentry); + if (pinentry.error) { + free(pinentry.error); + pinentry.error = NULL; + } + + if (pinentry.close_button) assuan_write_status(ctx, "BUTTON_INFO", "close"); + + if (result > 0) return 0; /* OK */ + + if (pinentry.specific_err) { + write_status_error(ctx, &pinentry); + + if (gpg_err_code(pinentry.specific_err) == GPG_ERR_FULLY_CANCELED) + assuan_set_flag(ctx, ASSUAN_FORCE_CLOSE, 1); + + return pinentry.specific_err; + } + + if (pinentry.locale_err) return gpg_error(GPG_ERR_LOCALE_PROBLEM); + + if (pinentry.one_button) return 0; /* OK */ + + if (pinentry.canceled) return gpg_error(GPG_ERR_CANCELED); + return gpg_error(GPG_ERR_NOT_CONFIRMED); +} + +static gpg_error_t cmd_message(assuan_context_t ctx, char *line) { + (void)line; + + return cmd_confirm(ctx, "--one-button"); +} + +/* Return a staically allocated string with information on the mode, + * uid, and gid of DEVICE. On error "?" is returned if DEVICE is + * NULL, "-" is returned. */ +static const char *device_stat_string(const char *device) { +#ifdef HAVE_STAT + static char buf[40]; + struct stat st; + + if (!device || !*device) return "-"; + + if (stat(device, &st)) return "?"; /* Error */ + snprintf(buf, sizeof buf, "%lo/%lu/%lu", (unsigned long)st.st_mode, + (unsigned long)st.st_uid, (unsigned long)st.st_gid); + return buf; +#else + return "-"; +#endif +} + +/* GETINFO <what> + + Multipurpose function to return a variety of information. + Supported values for WHAT are: + + version - Return the version of the program. + pid - Return the process id of the server. + flavor - Return information about the used pinentry flavor + ttyinfo - Return DISPLAY, ttyinfo and an emacs pinentry status + */ +static gpg_error_t cmd_getinfo(assuan_context_t ctx, char *line) { + int rc; + const char *s; + char buffer[150]; + + if (!strcmp(line, "version")) { + s = 0; + rc = assuan_send_data(ctx, s, strlen(s)); + } else if (!strcmp(line, "pid")) { + snprintf(buffer, sizeof buffer, "%lu", (unsigned long)getpid()); + rc = assuan_send_data(ctx, buffer, strlen(buffer)); + } else if (!strcmp(line, "flavor")) { + if (!strncmp(this_pgmname, "pinentry-", 9) && this_pgmname[9]) + s = this_pgmname + 9; + else + s = this_pgmname; + + snprintf(buffer, sizeof buffer, "%s%s%s", s, flavor_flag ? ":" : "", + flavor_flag ? flavor_flag : ""); + rc = assuan_send_data(ctx, buffer, strlen(buffer)); + /* if (!rc) */ + /* rc = assuan_write_status (ctx, "FEATURES", "tabbing foo bar"); */ + } else if (!strcmp(line, "ttyinfo")) { + char emacs_status[10]; +#ifdef INSIDE_EMACS + snprintf(emacs_status, sizeof emacs_status, "%d", pinentry_emacs_status()); +#else + strcpy(emacs_status, "-"); +#endif + snprintf(buffer, sizeof buffer, "%s %s %s %s %lu/%lu %s", + pinentry.ttyname ? pinentry.ttyname : "-", + pinentry.ttytype_l ? pinentry.ttytype_l : "-", + pinentry.display ? pinentry.display : "-", + device_stat_string(pinentry.ttyname), +#ifdef HAVE_DOSISH_SYSTEM + 0l, 0l, +#else + (unsigned long)geteuid(), (unsigned long)getegid(), +#endif + emacs_status); + rc = assuan_send_data(ctx, buffer, strlen(buffer)); + } else + rc = gpg_error(GPG_ERR_ASS_PARAMETER); + return rc; +} + +/* CLEARPASSPHRASE <cacheid> + + Clear the cache passphrase associated with the key identified by + cacheid. + */ +static gpg_error_t cmd_clear_passphrase(assuan_context_t ctx, char *line) { + (void)ctx; + + if (!line) return gpg_error(GPG_ERR_ASS_INV_VALUE); + + /* Remove leading and trailing white space. */ + while (*line == ' ') line++; + while (line[strlen(line) - 1] == ' ') line[strlen(line) - 1] = 0; + + switch (password_cache_clear(line)) { + case 1: + return 0; + case 0: + return gpg_error(GPG_ERR_ASS_INV_VALUE); + default: + return gpg_error(GPG_ERR_ASS_GENERAL); + } +} + +/* Tell the assuan library about our commands. */ +static gpg_error_t register_commands(assuan_context_t ctx) { + static struct { + const char *name; + gpg_error_t (*handler)(assuan_context_t, char *line); + } table[] = {{"SETDESC", cmd_setdesc}, + {"SETPROMPT", cmd_setprompt}, + {"SETKEYINFO", cmd_setkeyinfo}, + {"SETREPEAT", cmd_setrepeat}, + {"SETREPEATERROR", cmd_setrepeaterror}, + {"SETREPEATOK", cmd_setrepeatok}, + {"SETERROR", cmd_seterror}, + {"SETOK", cmd_setok}, + {"SETNOTOK", cmd_setnotok}, + {"SETCANCEL", cmd_setcancel}, + {"GETPIN", cmd_getpin}, + {"CONFIRM", cmd_confirm}, + {"MESSAGE", cmd_message}, + {"SETQUALITYBAR", cmd_setqualitybar}, + {"SETQUALITYBAR_TT", cmd_setqualitybar_tt}, + {"SETGENPIN", cmd_setgenpin_label}, + {"SETGENPIN_TT", cmd_setgenpin_tt}, + {"GETINFO", cmd_getinfo}, + {"SETTITLE", cmd_settitle}, + {"SETTIMEOUT", cmd_settimeout}, + {"CLEARPASSPHRASE", cmd_clear_passphrase}, + {NULL}}; + int i, j; + gpg_error_t rc; + + for (i = j = 0; table[i].name; i++) { + rc = assuan_register_command(ctx, table[i].name, table[i].handler, NULL); + if (rc) return rc; + } + return 0; +} + +int pinentry_loop2(int infd, int outfd) { + gpg_error_t rc; + assuan_fd_t filedes[2]; + assuan_context_t ctx; + + /* Extra check to make sure we have dropped privs. */ +#ifndef HAVE_DOSISH_SYSTEM + if (getuid() != geteuid()) abort(); +#endif + + rc = assuan_new(&ctx); + if (rc) { + fprintf(stderr, "server context creation failed: %s\n", gpg_strerror(rc)); + return -1; + } + + /* For now we use a simple pipe based server so that we can work + from scripts. We will later add options to run as a daemon and + wait for requests on a Unix domain socket. */ + filedes[0] = assuan_fdopen(infd); + filedes[1] = assuan_fdopen(outfd); + rc = assuan_init_pipe_server(ctx, filedes); + if (rc) { + fprintf(stderr, "%s: failed to initialize the server: %s\n", this_pgmname, + gpg_strerror(rc)); + return -1; + } + rc = register_commands(ctx); + if (rc) { + fprintf(stderr, "%s: failed to the register commands with Assuan: %s\n", + this_pgmname, gpg_strerror(rc)); + return -1; + } + + assuan_register_option_handler(ctx, option_handler); +#if 0 + assuan_set_log_stream (ctx, stderr); +#endif + assuan_register_reset_notify(ctx, pinentry_assuan_reset_handler); + + for (;;) { + rc = assuan_accept(ctx); + if (rc == -1) + break; + else if (rc) { + fprintf(stderr, "%s: Assuan accept problem: %s\n", this_pgmname, + gpg_strerror(rc)); + break; + } + + rc = assuan_process(ctx); + if (rc) { + fprintf(stderr, "%s: Assuan processing failed: %s\n", this_pgmname, + gpg_strerror(rc)); + continue; + } + } + + assuan_release(ctx); + return 0; +} + +/* Start the pinentry event loop. The program will start to process + Assuan commands until it is finished or an error occurs. If an + error occurs, -1 is returned. Otherwise, 0 is returned. */ +int pinentry_loop(void) { return pinentry_loop2(STDIN_FILENO, STDOUT_FILENO); } diff --git a/src/pinentry/pinentry.h b/src/pinentry/pinentry.h new file mode 100644 index 00000000..aec01946 --- /dev/null +++ b/src/pinentry/pinentry.h @@ -0,0 +1,370 @@ +/* pinentry.h - The interface for the PIN entry support library. + * Copyright (C) 2002, 2003, 2010, 2015, 2021 g10 Code GmbH + * + * This file is part of PINENTRY. + * + * PINENTRY 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 2 of the License, or + * (at your option) any later version. + * + * PINENTRY 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef PINENTRY_H +#define PINENTRY_H + +#include "secmem.h" + +#ifdef __cplusplus +extern "C" { +#if 0 +} +#endif +#endif + +typedef enum { + PINENTRY_COLOR_NONE, + PINENTRY_COLOR_DEFAULT, + PINENTRY_COLOR_BLACK, + PINENTRY_COLOR_RED, + PINENTRY_COLOR_GREEN, + PINENTRY_COLOR_YELLOW, + PINENTRY_COLOR_BLUE, + PINENTRY_COLOR_MAGENTA, + PINENTRY_COLOR_CYAN, + PINENTRY_COLOR_WHITE +} pinentry_color_t; + +struct pinentry { + /* The window title, or NULL. (Assuan: "SETTITLE TITLE".) */ + char *title; + /* The description to display, or NULL. (Assuan: "SETDESC + DESC".) */ + char *description; + /* The error message to display, or NULL. (Assuan: "SETERROR + MESSAGE".) */ + char *error; + /* The prompt to display, or NULL. (Assuan: "SETPROMPT + prompt".) */ + char *prompt; + /* The OK button text to display, or NULL. (Assuan: "SETOK + OK".) */ + char *ok; + /* The Not-OK button text to display, or NULL. This is the text for + the alternative option shown by the third button. (Assuan: + "SETNOTOK NOTOK".) */ + char *notok; + /* The Cancel button text to display, or NULL. (Assuan: "SETCANCEL + CANCEL".) */ + char *cancel; + + /* The buffer to store the secret into. */ + char *pin; + /* The length of the buffer. */ + int pin_len; + /* Whether the pin was read from an external cache (1) or entered by + the user (0). */ + int pin_from_cache; + + /* The name of the X display to use if X is available and supported. + (Assuan: "OPTION display DISPLAY".) */ + char *display; + /* The name of the terminal node to open if X not available or + supported. (Assuan: "OPTION ttyname TTYNAME".) */ + char *ttyname; + /* The type of the terminal. (Assuan: "OPTION ttytype TTYTYPE".) */ + char *ttytype_l; + /* Set the alert mode (none, beep or flash). */ + char *ttyalert; + /* The LC_CTYPE value for the terminal. (Assuan: "OPTION lc-ctype + LC_CTYPE".) */ + char *lc_ctype; + /* The LC_MESSAGES value for the terminal. (Assuan: "OPTION + lc-messages LC_MESSAGES".) */ + char *lc_messages; + + /* True if debug mode is requested. */ + int debug; + + /* The number of seconds before giving up while waiting for user input. */ + int timeout; + + /* True if caller should grab the keyboard. (Assuan: "OPTION grab" + or "OPTION no-grab".) */ + int grab; + + /* The PID of the owner or 0 if not known. The owner is the process + * which actually triggered the the pinentry. For example gpg. */ + unsigned long owner_pid; + + /* The numeric uid (user ID) of the owner process or -1 if not + * known. */ + int owner_uid; + + /* The malloced hostname of the owner or NULL. */ + char *owner_host; + + /* The window ID of the parent window over which the pinentry window + should be displayed. (Assuan: "OPTION parent-wid WID".) */ + int parent_wid; + + /* The name of an optional file which will be touched after a curses + entry has been displayed. (Assuan: "OPTION touch-file + FILENAME".) */ + char *touch_file; + + /* The frontend should set this to -1 if the user canceled the + request, and to the length of the PIN stored in pin + otherwise. */ + int result; + + /* The frontend should set this if the NOTOK button was pressed. */ + int canceled; + + /* The frontend should set this to true if an error with the local + conversion occurred. */ + int locale_err; + + /* The frontend should set this to a gpg-error so that commands are + able to return specific error codes. This is an ugly hack due to + the fact that pinentry_cmd_handler_t returns the length of the + passphrase or a negative error code. */ + int specific_err; + + /* The frontend may store a string with the error location here. */ + const char *specific_err_loc; + + /* The frontend may store a malloced string here to emit an ERROR + * status code with this extra info along with SPECIFIC_ERR. */ + char *specific_err_info; + + /* The frontend should set this to true if the window close button + has been used. This flag is used in addition to a regular return + value. */ + int close_button; + + /* The caller should set this to true if only one button is + required. This is useful for notification dialogs where only a + dismiss button is required. */ + int one_button; + + /* Whether this is a CONFIRM pinentry. */ + int confirm; + + /* If true a second prompt for the passphrase is shown and the user + is expected to enter the same passphrase again. Pinentry checks + that both match. (Assuan: "SETREPEAT".) */ + char *repeat_passphrase; + + /* The string to show if a repeated passphrase does not match. + (Assuan: "SETREPEATERROR ERROR".) */ + char *repeat_error_string; + + /* The string to show if a repeated passphrase does match. + (Assuan: "SETREPEATOK STRING".) */ + char *repeat_ok_string; + + /* Set to true if the passphrase has been entered a second time and + matches the first passphrase. */ + int repeat_okay; + + /* If this is not NULL, a passphrase quality indicator is shown. + There will also be an inquiry back to the caller to get an + indication of the quality for the passphrase entered so far. The + string is used as a label for the quality bar. (Assuan: + "SETQUALITYBAR LABEL".) */ + char *quality_bar; + + /* The tooltip to be shown for the qualitybar. Malloced or NULL. + (Assuan: "SETQUALITYBAR_TT TOOLTIP".) */ + char *quality_bar_tt; + + /* If this is not NULL, a generate action should be shown. + There will be an inquiry back to the caller to get such a + PIN. generate action. Malloced or NULL. + (Assuan: "SETGENPIN LABEL" .) */ + char *genpin_label; + + /* The tooltip to be shown for the generate action. Malloced or NULL. + (Assuan: "SETGENPIN_TT TOOLTIP".) */ + char *genpin_tt; + + /* Specifies whether passphrase formatting should be enabled. + (Assuan: "OPTION formatted-passphrase") */ + int formatted_passphrase; + + /* A hint to be shown near the passphrase input field if passphrase + formatting is enabled. Malloced or NULL. + (Assuan: "OPTION formatted-passphrase-hint=HINT".) */ + char *formatted_passphrase_hint; + + /* For the curses pinentry, the color of error messages. */ + pinentry_color_t color_fg; + int color_fg_bright; + pinentry_color_t color_bg; + pinentry_color_t color_so; + int color_so_bright; + pinentry_color_t color_ok; + int color_ok_bright; + pinentry_color_t color_qualitybar; + int color_qualitybar_bright; + + /* Malloced and i18ned default strings or NULL. These strings may + include an underscore character to indicate an accelerator key. + A double underscore represents a plain one. */ + /* (Assuan: "OPTION default-ok OK"). */ + char *default_ok; + /* (Assuan: "OPTION default-cancel CANCEL"). */ + char *default_cancel; + /* (Assuan: "OPTION default-prompt PROMPT"). */ + char *default_prompt; + /* (Assuan: "OPTION default-pwmngr + SAVE_PASSWORD_WITH_PASSWORD_MANAGER?"). */ + char *default_pwmngr; + /* (Assuan: "OPTION default-cf-visi + Do you really want to make your passphrase visible?"). */ + char *default_cf_visi; + /* (Assuan: "OPTION default-tt-visi + Make passphrase visible?"). */ + char *default_tt_visi; + /* (Assuan: "OPTION default-tt-hide + Hide passphrase"). */ + char *default_tt_hide; + /* (Assuan: "OPTION default-capshint + Caps Lock is on"). */ + char *default_capshint; + + /* Whether we are allowed to read the password from an external + cache. (Assuan: "OPTION allow-external-password-cache") */ + int allow_external_password_cache; + + /* We only try the cache once. */ + int tried_password_cache; + + /* A stable identifier for the key. (Assuan: "SETKEYINFO + KEYINFO".) */ + char *keyinfo; + + /* Whether we may cache the password (according to the user). */ + int may_cache_password; + + /* NOTE: If you add any additional fields to this structure, be sure + to update the initializer in pinentry/pinentry.c!!! */ + + /* For the quality indicator and genpin we need to do an inquiry. + Thus we need to save the assuan ctx. */ + void *ctx_assuan; + + /* An UTF-8 string with an invisible character used to override the + default in some pinentries. Only the first character is + used. */ + char *invisible_char; + + /* Whether the passphrase constraints are enforced by gpg-agent. + (Assuan: "OPTION constraints-enforce") */ + int constraints_enforce; + + /* A short translated hint for the user with the constraints for new + passphrases to be displayed near the passphrase input field. + Malloced or NULL. + (Assuan: "OPTION constraints-hint-short=At least 8 characters".) */ + char *constraints_hint_short; + + /* A longer translated hint for the user with the constraints for new + passphrases to be displayed for example as tooltip. Malloced or NULL. + (Assuan: "OPTION constraints-hint-long=The passphrase must ...".) */ + char *constraints_hint_long; + + /* A short translated title for an error dialog informing the user about + unsatisfied passphrase constraints. Malloced or NULL. + (Assuan: "OPTION constraints-error-title=Passphrase Not Allowed".) */ + char *constraints_error_title; +}; +typedef struct pinentry *pinentry_t; + +/* The pinentry command handler type processes the pinentry request + PIN. If PIN->pin is zero, request a confirmation, otherwise a PIN + entry. On confirmation, the function should return TRUE if + confirmed, and FALSE otherwise. On PIN entry, the function should + return -1 if an error occurred or the user cancelled the operation + and 1 otherwise. */ +typedef int (*pinentry_cmd_handler_t)(pinentry_t pin); + +/* Start the pinentry event loop. The program will start to process + Assuan commands until it is finished or an error occurs. If an + error occurs, -1 is returned and errno indicates the type of an + error. Otherwise, 0 is returned. */ +int pinentry_loop(void); + +/* The same as above but allows to specify the i/o descriptors. + * infd and outfd will be duplicated in this function so the caller + * still has to close them if necessary. + */ +int pinentry_loop2(int infd, int outfd); + +const char *pinentry_get_pgmname(void); + +char *pinentry_get_title(pinentry_t pe); + +/* Run a quality inquiry for PASSPHRASE of LENGTH. */ +int pinentry_inq_quality(pinentry_t pin, const char *passphrase, size_t length); + +/* Run a checkpin inquiry for PASSPHRASE of LENGTH. Returns NULL, if the + passphrase satisfies the constraints. Otherwise, returns a malloced error + string. */ +char *pinentry_inq_checkpin(pinentry_t pin, const char *passphrase, + size_t length); + +/* Run a genpin iquriry. Returns a malloced string or NULL */ +char *pinentry_inq_genpin(pinentry_t pin); + +/* Try to make room for at least LEN bytes for the pin in the pinentry + PIN. Returns new buffer on success and 0 on failure. */ +char *pinentry_setbufferlen(pinentry_t pin, int len); + +/* Use the buffer at BUFFER for PIN->PIN. BUFFER must be NULL or + allocated using secmem_alloc. LEN is the size of the buffer. If + it is unknown, but BUFFER is a NUL terminated string, you pass 0 to + just use strlen(buffer)+1. */ +void pinentry_setbuffer_use(pinentry_t pin, char *buffer, int len); + +/* Initialize the secure memory subsystem, drop privileges and + return. Must be called early. */ +void pinentry_init(const char *pgmname); + +/* Return true if either DISPLAY is set or ARGV contains the string + "--display". */ +int pinentry_have_display(int argc, char **argv); + +/* Parse the command line options. May exit the program if only help + or version output is requested. */ +void pinentry_parse_opts(int argc, char *argv[]); + +/* Set the optional flag used with getinfo. */ +void pinentry_set_flavor_flag(const char *string); + +/* The caller must define this variable to process assuan commands. */ +extern pinentry_cmd_handler_t pinentry_cmd_handler; + +#ifdef WINDOWS +/* Windows declares sleep as obsolete, but provides a definition for + _sleep but non for the still existing sleep. */ +#define sleep(a) _sleep((a)) +#endif /*WINDOWS*/ + +#if 0 +{ +#endif +#ifdef __cplusplus +} +#endif + +#endif /* PINENTRY_H */ diff --git a/src/pinentry/pinentry_debug.cpp b/src/pinentry/pinentry_debug.cpp new file mode 100644 index 00000000..9afbcdb3 --- /dev/null +++ b/src/pinentry/pinentry_debug.cpp @@ -0,0 +1,31 @@ +/* pinentry_debug.h - Logging category for pinentry + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "pinentry_debug.h" + +#if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) +Q_LOGGING_CATEGORY(PINENTRY_LOG, "gpg.pinentry", QtWarningMsg) +#else +Q_LOGGING_CATEGORY(PINENTRY_LOG, "gpg.pinentry") +#endif diff --git a/src/pinentry/pinentry_debug.h b/src/pinentry/pinentry_debug.h new file mode 100644 index 00000000..fc8c808a --- /dev/null +++ b/src/pinentry/pinentry_debug.h @@ -0,0 +1,28 @@ +/* pinentry_debug.h - Logging category for pinentry + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_DEBUG_H__ +#define __PINENTRY_QT_DEBUG_H__ + +#include <QLoggingCategory> + +Q_DECLARE_LOGGING_CATEGORY(PINENTRY_LOG) + +#endif // __PINENTRY_QT_DEBUG_H__ diff --git a/src/pinentry/pinentry_main.cpp b/src/pinentry/pinentry_main.cpp new file mode 100644 index 00000000..fcea7c31 --- /dev/null +++ b/src/pinentry/pinentry_main.cpp @@ -0,0 +1,413 @@ +/* main.cpp - A Qt dialog for PIN entry. + * Copyright (C) 2002, 2008 Klarälvdalens Datakonsult AB (KDAB) + * Copyright (C) 2003, 2021 g10 Code GmbH + * Copyright 2007 Ingo Klöcker + * + * Written by Steffen Hansen <[email protected]>. + * Modified by Marcus Brinkmann <[email protected]>. + * Modified by Marc Mutz <[email protected]> + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <QApplication> +#include <QDebug> +#include <QIcon> +#include <QMessageBox> +#include <QPushButton> +#include <QString> +#include <QWidget> + +#include "accessibility.h" +#include "keyboardfocusindication.h" +#include "pinentry.h" +#include "pinentryconfirm.h" +#include "pinentrydialog.h" +#include "util.h" +#if QT_VERSION >= 0x050000 +#include <QWindow> +#endif + +#include <errno.h> +#include <gpg-error.h> +#include <stdio.h> + +#include <stdexcept> + +#ifdef FALLBACK_CURSES +#include <pinentry-curses.h> +#endif + +#if QT_VERSION >= 0x050000 && defined(QT_STATIC) +#include <QtPlugin> +#ifdef Q_OS_WIN +#include <shlobj.h> +#include <windows.h> +Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin) +#elif defined(Q_OS_MAC) +Q_IMPORT_PLUGIN(QCocoaIntegrationPlugin) +#else +Q_IMPORT_PLUGIN(QXcbIntegrationPlugin) +#endif +#endif + +#ifdef Q_OS_WIN +#include <windows.h> +#endif + +#include "pinentry_debug.h" + +static QString escape_accel(const QString &s) { + QString result; + result.reserve(s.size()); + + bool afterUnderscore = false; + + for (unsigned int i = 0, end = s.size(); i != end; ++i) { + const QChar ch = s[i]; + if (ch == QLatin1Char('_')) { + if (afterUnderscore) { // escaped _ + result += QLatin1Char('_'); + afterUnderscore = false; + } else { // accel + afterUnderscore = true; + } + } else { + if (afterUnderscore || // accel + ch == QLatin1Char('&')) { // escape & from being interpreted by Qt + result += QLatin1Char('&'); + } + result += ch; + afterUnderscore = false; + } + } + + if (afterUnderscore) + // trailing single underscore: shouldn't happen, but deal with it robustly: + { + result += QLatin1Char('_'); + } + + return result; +} + +namespace { +class InvalidUtf8 : public std::invalid_argument { + public: + InvalidUtf8() : std::invalid_argument("invalid utf8") {} + ~InvalidUtf8() throw() {} +}; +} // namespace + +static const bool GPG_AGENT_IS_PORTED_TO_ONLY_SEND_UTF8 = false; + +static QString from_utf8(const char *s) { + const QString result = QString::fromUtf8(s); + if (result.contains(QChar::ReplacementCharacter)) { + if (GPG_AGENT_IS_PORTED_TO_ONLY_SEND_UTF8) { + throw InvalidUtf8(); + } else { + return QString::fromLocal8Bit(s); + } + } + + return result; +} + +static void setup_foreground_window(QWidget *widget, WId parentWid) { +#if QT_VERSION >= 0x050000 + /* For windows set the desktop window as the transient parent */ + QWindow *parentWindow = nullptr; + if (parentWid) { + parentWindow = QWindow::fromWinId(parentWid); + } +#ifdef Q_OS_WIN + if (!parentWindow) { + HWND desktop = GetDesktopWindow(); + if (desktop) { + parentWindow = QWindow::fromWinId((WId)desktop); + } + } +#endif + if (parentWindow) { + // Ensure that we have a native wid + widget->winId(); + QWindow *wndHandle = widget->windowHandle(); + + if (wndHandle) { + wndHandle->setTransientParent(parentWindow); + } + } +#endif + widget->setWindowFlags(Qt::Window | Qt::CustomizeWindowHint | + Qt::WindowTitleHint | Qt::WindowCloseButtonHint | + Qt::WindowStaysOnTopHint | + Qt::WindowMinimizeButtonHint); +} + +static int qt_cmd_handler(pinentry_t pe) { + int want_pass = !!pe->pin; + + const QString ok = pe->ok ? escape_accel(from_utf8(pe->ok)) + : pe->default_ok ? escape_accel(from_utf8(pe->default_ok)) + : + /* else */ QLatin1String("&OK"); + const QString cancel = pe->cancel ? escape_accel(from_utf8(pe->cancel)) + : pe->default_cancel + ? escape_accel(from_utf8(pe->default_cancel)) + : + /* else */ QLatin1String("&Cancel"); + + unique_malloced_ptr<char> str{pinentry_get_title(pe)}; + const QString title = str ? from_utf8(str.get()) : + /* else */ QLatin1String("pinentry-qt"); + + const QString repeatError = pe->repeat_error_string + ? from_utf8(pe->repeat_error_string) + : QLatin1String("Passphrases do not match"); + const QString repeatString = + pe->repeat_passphrase ? from_utf8(pe->repeat_passphrase) : QString(); + const QString visibilityTT = pe->default_tt_visi + ? from_utf8(pe->default_tt_visi) + : QLatin1String("Show passphrase"); + const QString hideTT = pe->default_tt_hide ? from_utf8(pe->default_tt_hide) + : QLatin1String("Hide passphrase"); + + const QString capsLockHint = pe->default_capshint + ? from_utf8(pe->default_capshint) + : QLatin1String("Caps Lock is on"); + + const QString generateLbl = + pe->genpin_label ? from_utf8(pe->genpin_label) : QString(); + const QString generateTT = + pe->genpin_tt ? from_utf8(pe->genpin_tt) : QString(); + + if (want_pass) { + PinEntryDialog pinentry(nullptr, 0, pe->timeout, true, !!pe->quality_bar, + repeatString, visibilityTT, hideTT); + setup_foreground_window(&pinentry, pe->parent_wid); + pinentry.setPinentryInfo(pe); + pinentry.setPrompt(escape_accel(from_utf8(pe->prompt))); + pinentry.setDescription(from_utf8(pe->description)); + pinentry.setRepeatErrorText(repeatError); + pinentry.setGenpinLabel(generateLbl); + pinentry.setGenpinTT(generateTT); + pinentry.setCapsLockHint(capsLockHint); + pinentry.setFormattedPassphrase({bool(pe->formatted_passphrase), + from_utf8(pe->formatted_passphrase_hint)}); + pinentry.setConstraintsOptions({bool(pe->constraints_enforce), + from_utf8(pe->constraints_hint_short), + from_utf8(pe->constraints_hint_long), + from_utf8(pe->constraints_error_title)}); + + if (!title.isEmpty()) { + pinentry.setWindowTitle(title); + } + + /* If we reuse the same dialog window. */ + pinentry.setPin(QString()); + + pinentry.setOkText(ok); + pinentry.setCancelText(cancel); + if (pe->error) { + pinentry.setError(from_utf8(pe->error)); + } + if (pe->quality_bar) { + pinentry.setQualityBar(from_utf8(pe->quality_bar)); + } + if (pe->quality_bar_tt) { + pinentry.setQualityBarTT(from_utf8(pe->quality_bar_tt)); + } + bool ret = pinentry.exec(); + if (!ret) { + if (pinentry.timedOut()) pe->specific_err = gpg_error(GPG_ERR_TIMEOUT); + return -1; + } + + const QString pinStr = pinentry.pin(); + QByteArray pin = pinStr.toUtf8(); + + if (!!pe->repeat_passphrase) { + /* Should not have been possible to accept + the dialog in that case but we do a safety + check here */ + pe->repeat_okay = (pinStr == pinentry.repeatedPin()); + } + + int len = strlen(pin.constData()); + if (len >= 0) { + pinentry_setbufferlen(pe, len + 1); + if (pe->pin) { + strcpy(pe->pin, pin.constData()); + return len; + } + } + return -1; + } else { + const QString desc = + pe->description ? from_utf8(pe->description) : QString(); + const QString notok = + pe->notok ? escape_accel(from_utf8(pe->notok)) : QString(); + + const QMessageBox::StandardButtons buttons = + pe->one_button ? QMessageBox::Ok + : pe->notok ? QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel + : + /* else */ QMessageBox::Ok | QMessageBox::Cancel; + + PinentryConfirm box{QMessageBox::Information, title, desc, buttons}; + box.setTextFormat(Qt::PlainText); + box.setTextInteractionFlags(Qt::TextSelectableByMouse); + box.setTimeout(std::chrono::seconds{pe->timeout}); + setup_foreground_window(&box, pe->parent_wid); + + const struct { + QMessageBox::StandardButton button; + QString label; + } buttonLabels[] = { + {QMessageBox::Ok, ok}, + {QMessageBox::Yes, ok}, + {QMessageBox::No, notok}, + {QMessageBox::Cancel, cancel}, + }; + + for (size_t i = 0; i < sizeof buttonLabels / sizeof *buttonLabels; ++i) + if ((buttons & buttonLabels[i].button) && + !buttonLabels[i].label.isEmpty()) { + box.button(buttonLabels[i].button)->setText(buttonLabels[i].label); + Accessibility::setDescription(box.button(buttonLabels[i].button), + buttonLabels[i].label); + } + + box.setIconPixmap(applicationIconPixmap()); + + if (!pe->one_button) { + box.setDefaultButton(QMessageBox::Cancel); + } + + box.show(); + raiseWindow(&box); + + const int rc = box.exec(); + + if (rc == QMessageBox::Cancel) { + pe->canceled = true; + } + if (box.timedOut()) { + pe->specific_err = gpg_error(GPG_ERR_TIMEOUT); + } + + return rc == QMessageBox::Ok || rc == QMessageBox::Yes; + } +} + +static int qt_cmd_handler_ex(pinentry_t pe) { + try { + return qt_cmd_handler(pe); + } catch (const InvalidUtf8 &) { + pe->locale_err = true; + return pe->pin ? -1 : false; + } catch (...) { + pe->canceled = true; + return pe->pin ? -1 : false; + } +} + +pinentry_cmd_handler_t pinentry_cmd_handler = qt_cmd_handler_ex; + +int pinentry_main(int argc, char *argv[]) { + pinentry_init("pinentry-qt"); + + QApplication *app = NULL; + int new_argc = 0; + +#ifdef FALLBACK_CURSES +#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) + // check a few environment variables that are usually set on X11 or Wayland + // sessions + const bool hasWaylandDisplay = qEnvironmentVariableIsSet("WAYLAND_DISPLAY"); + const bool isWaylandSessionType = qgetenv("XDG_SESSION_TYPE") == "wayland"; + const bool hasX11Display = pinentry_have_display(argc, argv); + const bool isX11SessionType = qgetenv("XDG_SESSION_TYPE") == "x11"; + const bool isGUISession = hasWaylandDisplay || isWaylandSessionType || + hasX11Display || isX11SessionType; + qCDebug(PINENTRY_LOG) << "hasWaylandDisplay:" << hasWaylandDisplay; + qCDebug(PINENTRY_LOG) << "isWaylandSessionType:" << isWaylandSessionType; + qCDebug(PINENTRY_LOG) << "hasX11Display:" << hasX11Display; + qCDebug(PINENTRY_LOG) << "isX11SessionType:" << isX11SessionType; + qCDebug(PINENTRY_LOG) << "isGUISession:" << isGUISession; +#else + const bool isGUISession = pinentry_have_display(argc, argv); +#endif + if (!isGUISession) { + pinentry_cmd_handler = curses_cmd_handler; + pinentry_set_flavor_flag("curses"); + } else +#endif + { + /* Qt does only understand -display but not --display; thus we + are fixing that here. The code is pretty simply and may get + confused if an argument is called "--display". */ + char **new_argv, *p; + size_t n; + int i, done; + + for (n = 0, i = 0; i < argc; i++) { + n += strlen(argv[i]) + 1; + } + n++; + new_argv = (char **)calloc(argc + 1, sizeof *new_argv); + if (new_argv) { + *new_argv = (char *)malloc(n); + } + if (!new_argv || !*new_argv) { + fprintf(stderr, "pinentry-qt: can't fixup argument list: %s\n", + strerror(errno)); + exit(EXIT_FAILURE); + } + for (done = 0, p = *new_argv, i = 0; i < argc; i++) + if (!done && !strcmp(argv[i], "--display")) { + new_argv[i] = strcpy(p, argv[i] + 1); + p += strlen(argv[i] + 1) + 1; + done = 1; + } else { + new_argv[i] = strcpy(p, argv[i]); + p += strlen(argv[i]) + 1; + } + + /* Note: QApplication uses int &argc so argc has to be valid + * for the full lifetime of the application. + * + * As Qt might modify argc / argv we use copies here so that + * we do not loose options that are handled in both. e.g. display. + */ + new_argc = argc; + Q_ASSERT(new_argc); + app = new QApplication(new_argc, new_argv); + app->setWindowIcon(QIcon(QLatin1String(":/document-encrypt.png"))); + (void)new KeyboardFocusIndication{app}; + } + + pinentry_parse_opts(argc, argv); + + int rc = pinentry_loop(); + delete app; + return rc ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/src/pinentry/pinentryconfirm.cpp b/src/pinentry/pinentryconfirm.cpp new file mode 100644 index 00000000..31d55b5c --- /dev/null +++ b/src/pinentry/pinentryconfirm.cpp @@ -0,0 +1,123 @@ +/* pinentryconfirm.cpp - A QMessageBox with a timeout + * + * Copyright (C) 2011 Ben Kibbey <[email protected]> + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "pinentryconfirm.h" + +#include <QAbstractButton> +#include <QApplication> +#include <QFontMetrics> +#include <QGridLayout> +#include <QLabel> +#include <QSpacerItem> + +#include "accessibility.h" +#include "pinentrydialog.h" + +namespace { +QLabel *messageBoxLabel(QMessageBox *messageBox) { + return messageBox->findChild<QLabel *>(QStringLiteral("qt_msgbox_label")); +} +} // namespace + +PinentryConfirm::PinentryConfirm(Icon icon, const QString &title, + const QString &text, StandardButtons buttons, + QWidget *parent, Qt::WindowFlags flags) + : QMessageBox{icon, title, text, buttons, parent, flags} { + _timer.callOnTimeout(this, &PinentryConfirm::slotTimeout); + +#ifndef QT_NO_ACCESSIBILITY + QAccessible::installActivationObserver(this); + accessibilityActiveChanged(QAccessible::isActive()); +#endif + +#if QT_VERSION >= 0x050000 + /* This is in line with PinentryDialog ctor to have a maximizing + * animation when opening. */ + if (qApp->platformName() != QLatin1String("wayland")) { + setWindowState(Qt::WindowMinimized); + QTimer::singleShot(0, this, [this]() { raiseWindow(this); }); + } +#else + activateWindow(); + raise(); +#endif +} + +PinentryConfirm::~PinentryConfirm() { +#ifndef QT_NO_ACCESSIBILITY + QAccessible::removeActivationObserver(this); +#endif +} + +void PinentryConfirm::setTimeout(std::chrono::seconds timeout) { + _timer.setInterval(timeout); +} + +std::chrono::seconds PinentryConfirm::timeout() const { + return std::chrono::duration_cast<std::chrono::seconds>( + _timer.intervalAsDuration()); +} + +bool PinentryConfirm::timedOut() const { return _timed_out; } + +void PinentryConfirm::showEvent(QShowEvent *event) { + static bool resized; + if (!resized) { + QGridLayout *lay = dynamic_cast<QGridLayout *>(layout()); + if (lay) { + QSize textSize = fontMetrics().size(Qt::TextExpandTabs, text(), + fontMetrics().maxWidth()); + QSpacerItem *horizontalSpacer = + new QSpacerItem(textSize.width() + iconPixmap().width(), 0, + QSizePolicy::Minimum, QSizePolicy::Expanding); + lay->addItem(horizontalSpacer, lay->rowCount(), 1, 1, + lay->columnCount() - 1); + } + resized = true; + } + + QMessageBox::showEvent(event); + + if (timeout() > std::chrono::milliseconds::zero()) { + _timer.setSingleShot(true); + _timer.start(); + } +} + +void PinentryConfirm::slotTimeout() { + QAbstractButton *b = button(QMessageBox::Cancel); + _timed_out = true; + + if (b) { + b->animateClick(); + } +} + +#ifndef QT_NO_ACCESSIBILITY +void PinentryConfirm::accessibilityActiveChanged(bool active) { + // Allow text label to get focus if accessibility is active + const auto focusPolicy = active ? Qt::StrongFocus : Qt::ClickFocus; + if (auto label = messageBoxLabel(this)) { + label->setFocusPolicy(focusPolicy); + } +} +#endif diff --git a/src/pinentry/pinentryconfirm.h b/src/pinentry/pinentryconfirm.h new file mode 100644 index 00000000..7be7c268 --- /dev/null +++ b/src/pinentry/pinentryconfirm.h @@ -0,0 +1,63 @@ +/* pinentryconfirm.h - A QMessageBox with a timeout + * + * Copyright (C) 2011 Ben Kibbey <[email protected]> + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef PINENTRYCONFIRM_H +#define PINENTRYCONFIRM_H + +#include <QAccessible> +#include <QMessageBox> +#include <QTimer> + +class PinentryConfirm : public QMessageBox +#ifndef QT_NO_ACCESSIBILITY + , public QAccessible::ActivationObserver +#endif +{ + Q_OBJECT +public: + PinentryConfirm(Icon icon, const QString &title, const QString &text, + StandardButtons buttons = NoButton, QWidget *parent = nullptr, + Qt::WindowFlags flags = Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint); + ~PinentryConfirm() override; + + void setTimeout(std::chrono::seconds timeout); + std::chrono::seconds timeout() const; + + bool timedOut() const; + +protected: + void showEvent(QShowEvent *event) override; + +private Q_SLOTS: + void slotTimeout(); + +private: +#ifndef QT_NO_ACCESSIBILITY + void accessibilityActiveChanged(bool active) override; +#endif + +private: + QTimer _timer; + bool _timed_out = false; +}; + +#endif diff --git a/src/pinentry/pinentrydialog.cpp b/src/pinentry/pinentrydialog.cpp new file mode 100644 index 00000000..f0fba73c --- /dev/null +++ b/src/pinentry/pinentrydialog.cpp @@ -0,0 +1,718 @@ +/* pinentrydialog.cpp - A (not yet) secure Qt 4 dialog for PIN entry. + * Copyright (C) 2002, 2008 Klarälvdalens Datakonsult AB (KDAB) + * Copyright 2007 Ingo Klöcker + * Copyright 2016 Intevation GmbH + * Copyright (C) 2021, 2022 g10 Code GmbH + * + * Written by Steffen Hansen <[email protected]>. + * Modified by Andre Heinecke <[email protected]> + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <qnamespace.h> +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <QAccessible> +#include <QAction> +#include <QApplication> +#include <QCheckBox> +#include <QDebug> +#include <QDialogButtonBox> +#include <QFontMetrics> +#include <QGridLayout> +#include <QHBoxLayout> +#include <QKeyEvent> +#include <QLabel> +#include <QLineEdit> +#include <QMessageBox> +#include <QPainter> +#include <QPalette> +#include <QProgressBar> +#include <QPushButton> +#include <QRegularExpression> +#include <QStyle> +#include <QVBoxLayout> + +#include "accessibility.h" +#include "capslock/capslock.h" +#include "pinentrydialog.h" +#include "pinlineedit.h" +#include "util.h" + +#ifdef Q_OS_WIN +#include <windows.h> +#if QT_VERSION >= 0x050700 +#include <QtPlatformHeaders/QWindowsWindowFunctions> +#endif +#endif + +void raiseWindow(QWidget *w) { +#ifdef Q_OS_WIN +#if QT_VERSION >= 0x050700 + QWindowsWindowFunctions::setWindowActivationBehavior( + QWindowsWindowFunctions::AlwaysActivateWindow); +#endif +#endif + w->setWindowState((w->windowState() & ~Qt::WindowMinimized) | + Qt::WindowActive); + w->activateWindow(); + w->raise(); +} + +QPixmap applicationIconPixmap(const QIcon &overlayIcon) { + QPixmap pm = qApp->windowIcon().pixmap(48, 48); + + if (!overlayIcon.isNull()) { + QPainter painter(&pm); + const int emblemSize = 22; + painter.drawPixmap(pm.width() - emblemSize, 0, + overlayIcon.pixmap(emblemSize, emblemSize)); + } + + return pm; +} + +void PinEntryDialog::slotTimeout() { + _timed_out = true; + reject(); +} + +PinEntryDialog::PinEntryDialog(QWidget *parent, const char *name, int timeout, + bool modal, bool enable_quality_bar, + const QString &repeatString, + const QString &visibilityTT, + const QString &hideTT) + : QDialog{parent}, + _have_quality_bar{enable_quality_bar}, + mVisibilityTT{visibilityTT}, + mHideTT{hideTT} { + Q_UNUSED(name) + + if (modal) { + setModal(true); + } + + QPalette redTextPalette; + redTextPalette.setColor(QPalette::WindowText, Qt::red); + + auto *const mainLayout = new QVBoxLayout{this}; + + auto *const hbox = new QHBoxLayout; + + _icon = new QLabel(this); + _icon->setPixmap(applicationIconPixmap()); + hbox->addWidget(_icon, 0, Qt::AlignVCenter | Qt::AlignLeft); + + auto *const grid = new QGridLayout; + int row = 1; + + _error = new QLabel{this}; + _error->setTextFormat(Qt::PlainText); + _error->setTextInteractionFlags(Qt::TextSelectableByMouse); + _error->setPalette(redTextPalette); + _error->hide(); + grid->addWidget(_error, row, 1, 1, 2); + + row++; + _desc = new QLabel{this}; + _desc->setTextFormat(Qt::PlainText); + _desc->setTextInteractionFlags(Qt::TextSelectableByMouse); + _desc->hide(); + grid->addWidget(_desc, row, 1, 1, 2); + + row++; + mCapsLockHint = new QLabel{this}; + mCapsLockHint->setTextFormat(Qt::PlainText); + mCapsLockHint->setTextInteractionFlags(Qt::TextSelectableByMouse); + mCapsLockHint->setPalette(redTextPalette); + mCapsLockHint->setAlignment(Qt::AlignCenter); + mCapsLockHint->setVisible(false); + grid->addWidget(mCapsLockHint, row, 1, 1, 2); + + row++; + { + _prompt = new QLabel(this); + _prompt->setTextFormat(Qt::PlainText); + _prompt->setTextInteractionFlags(Qt::TextSelectableByMouse); + _prompt->hide(); + grid->addWidget(_prompt, row, 1); + + const auto l = new QHBoxLayout; + _edit = new PinLineEdit(this); + _edit->setMaxLength(256); + _edit->setMinimumWidth(_edit->fontMetrics().averageCharWidth() * 20 + 48); + _edit->setEchoMode(QLineEdit::Password); + _prompt->setBuddy(_edit); + l->addWidget(_edit, 1); + + if (!repeatString.isNull()) { + mGenerateButton = new QPushButton{this}; + mGenerateButton->setIcon(QIcon(QLatin1String(":/password-generate.svg"))); + mGenerateButton->setVisible(false); + l->addWidget(mGenerateButton); + } + grid->addLayout(l, row, 2); + } + + /* Set up the show password action */ + const QIcon visibilityIcon = QIcon(QLatin1String(":/visibility.svg")); + const QIcon hideIcon = QIcon(QLatin1String(":/hint.svg")); +#if QT_VERSION >= 0x050200 + if (!visibilityIcon.isNull() && !hideIcon.isNull()) { + mVisiActionEdit = + _edit->addAction(visibilityIcon, QLineEdit::TrailingPosition); + mVisiActionEdit->setVisible(false); + mVisiActionEdit->setToolTip(mVisibilityTT); + } else +#endif + { + if (!mVisibilityTT.isNull()) { + row++; + mVisiCB = new QCheckBox{mVisibilityTT, this}; + grid->addWidget(mVisiCB, row, 1, 1, 2, Qt::AlignLeft); + } + } + + row++; + mConstraintsHint = new QLabel{this}; + mConstraintsHint->setTextFormat(Qt::PlainText); + mConstraintsHint->setTextInteractionFlags(Qt::TextSelectableByMouse); + mConstraintsHint->setVisible(false); + grid->addWidget(mConstraintsHint, row, 2); + + row++; + mFormattedPassphraseHintSpacer = new QLabel{this}; + mFormattedPassphraseHintSpacer->setVisible(false); + mFormattedPassphraseHint = new QLabel{this}; + mFormattedPassphraseHint->setTextFormat(Qt::PlainText); + mFormattedPassphraseHint->setTextInteractionFlags(Qt::TextSelectableByMouse); + mFormattedPassphraseHint->setVisible(false); + grid->addWidget(mFormattedPassphraseHintSpacer, row, 1); + grid->addWidget(mFormattedPassphraseHint, row, 2); + + if (!repeatString.isNull()) { + row++; + auto repeatLabel = new QLabel{this}; + repeatLabel->setTextFormat(Qt::PlainText); + repeatLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + repeatLabel->setText(repeatString); + grid->addWidget(repeatLabel, row, 1); + + mRepeat = new PinLineEdit(this); + mRepeat->setMaxLength(256); + mRepeat->setEchoMode(QLineEdit::Password); + repeatLabel->setBuddy(mRepeat); + grid->addWidget(mRepeat, row, 2); + + row++; + mRepeatError = new QLabel{this}; + mRepeatError->setTextFormat(Qt::PlainText); + mRepeatError->setTextInteractionFlags(Qt::TextSelectableByMouse); + mRepeatError->setPalette(redTextPalette); + mRepeatError->hide(); + grid->addWidget(mRepeatError, row, 2); + } + + if (enable_quality_bar) { + row++; + _quality_bar_label = new QLabel(this); + _quality_bar_label->setTextFormat(Qt::PlainText); + _quality_bar_label->setTextInteractionFlags(Qt::TextSelectableByMouse); + _quality_bar_label->setAlignment(Qt::AlignVCenter); + grid->addWidget(_quality_bar_label, row, 1); + + _quality_bar = new QProgressBar(this); + _quality_bar->setAlignment(Qt::AlignCenter); + _quality_bar_label->setBuddy(_quality_bar); + grid->addWidget(_quality_bar, row, 2); + } + + hbox->addLayout(grid, 1); + mainLayout->addLayout(hbox); + + QDialogButtonBox *const buttons = new QDialogButtonBox(this); + buttons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + _ok = buttons->button(QDialogButtonBox::Ok); + _cancel = buttons->button(QDialogButtonBox::Cancel); + + if (style()->styleHint(QStyle::SH_DialogButtonBox_ButtonsHaveIcons)) { + _ok->setIcon(style()->standardIcon(QStyle::SP_DialogOkButton)); + _cancel->setIcon(style()->standardIcon(QStyle::SP_DialogCancelButton)); + } + + mainLayout->addStretch(1); + mainLayout->addWidget(buttons); + mainLayout->setSizeConstraint(QLayout::SetFixedSize); + + if (timeout > 0) { + _timer = new QTimer(this); + connect(_timer, &QTimer::timeout, this, &PinEntryDialog::slotTimeout); + _timer->start(timeout * 1000); + } + + connect(buttons, &QDialogButtonBox::accepted, this, + &PinEntryDialog::onAccept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(_edit, &QLineEdit::textChanged, this, &PinEntryDialog::updateQuality); + connect(_edit, &QLineEdit::textChanged, this, &PinEntryDialog::textChanged); + connect(_edit, &PinLineEdit::backspacePressed, this, + &PinEntryDialog::onBackspace); + if (mGenerateButton) { + connect(mGenerateButton, &QPushButton::clicked, this, + &PinEntryDialog::generatePin); + } + if (mVisiActionEdit) { + connect(mVisiActionEdit, &QAction::triggered, this, + &PinEntryDialog::toggleVisibility); + } + if (mVisiCB) { + connect(mVisiCB, &QCheckBox::toggled, this, + &PinEntryDialog::toggleVisibility); + } + if (mRepeat) { + connect(mRepeat, &QLineEdit::textChanged, this, + &PinEntryDialog::textChanged); + } + + auto capsLockWatcher = new CapsLockWatcher{this}; + connect(capsLockWatcher, &CapsLockWatcher::stateChanged, this, + [this](bool locked) { mCapsLockHint->setVisible(locked); }); + + connect(qApp, &QApplication::focusChanged, this, + &PinEntryDialog::focusChanged); + connect(qApp, &QApplication::applicationStateChanged, this, + &PinEntryDialog::checkCapsLock); + checkCapsLock(); + + setAttribute(Qt::WA_DeleteOnClose); + +#ifndef QT_NO_ACCESSIBILITY + QAccessible::installActivationObserver(this); + accessibilityActiveChanged(QAccessible::isActive()); +#endif + +#if QT_VERSION >= 0x050000 + /* This is mostly an issue on Windows where this results + in the pinentry popping up nicely with an animation and + comes to front. It is not ifdefed for Windows only since + window managers on Linux like KWin can also have this + result in an animation when the pinentry is shown and + not just popping it up. + */ + if (qApp->platformName() != QLatin1String("wayland")) { + setWindowState(Qt::WindowMinimized); + QTimer::singleShot(0, this, [this]() { raiseWindow(this); }); + } +#else + activateWindow(); + raise(); +#endif +} + +PinEntryDialog::~PinEntryDialog() { +#ifndef QT_NO_ACCESSIBILITY + QAccessible::removeActivationObserver(this); +#endif +} + +void PinEntryDialog::keyPressEvent(QKeyEvent *e) { + const auto returnPressed = + (!e->modifiers() && + (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return)) || + (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter); + if (returnPressed && _edit->hasFocus() && mRepeat) { + // if the user pressed Return in the first input field, then move the + // focus to the repeat input field and prevent further event processing + // by QDialog (which would trigger the default button) + mRepeat->setFocus(); + e->ignore(); + return; + } + + QDialog::keyPressEvent(e); +} + +void PinEntryDialog::keyReleaseEvent(QKeyEvent *event) { + QDialog::keyReleaseEvent(event); + checkCapsLock(); +} + +void PinEntryDialog::showEvent(QShowEvent *event) { + QDialog::showEvent(event); + _edit->setFocus(); +} + +void PinEntryDialog::setDescription(const QString &txt) { + _desc->setVisible(!txt.isEmpty()); + _desc->setText(txt); + _icon->setPixmap(applicationIconPixmap()); + setError(QString()); +} + +QString PinEntryDialog::description() const { return _desc->text(); } + +void PinEntryDialog::setError(const QString &txt) { + if (!txt.isNull()) { + _icon->setPixmap( + applicationIconPixmap(QIcon{QStringLiteral(":/data-error.svg")})); + } + _error->setText(txt); + _error->setVisible(!txt.isEmpty()); +} + +QString PinEntryDialog::error() const { return _error->text(); } + +void PinEntryDialog::setPin(const QString &txt) { _edit->setPin(txt); } + +QString PinEntryDialog::pin() const { return _edit->pin(); } + +void PinEntryDialog::setPrompt(const QString &txt) { + _prompt->setText(txt); + _prompt->setVisible(!txt.isEmpty()); + if (txt.contains("PIN")) _disable_echo_allowed = false; +} + +QString PinEntryDialog::prompt() const { return _prompt->text(); } + +void PinEntryDialog::setOkText(const QString &txt) { + _ok->setText(txt); + _ok->setVisible(!txt.isEmpty()); +} + +void PinEntryDialog::setCancelText(const QString &txt) { + _cancel->setText(txt); + _cancel->setVisible(!txt.isEmpty()); +} + +void PinEntryDialog::setQualityBar(const QString &txt) { + if (_have_quality_bar) { + _quality_bar_label->setText(txt); + } +} + +void PinEntryDialog::setQualityBarTT(const QString &txt) { + if (_have_quality_bar) { + _quality_bar->setToolTip(txt); + } +} + +void PinEntryDialog::setGenpinLabel(const QString &txt) { + if (!mGenerateButton) { + return; + } + mGenerateButton->setVisible(!txt.isEmpty()); + if (!txt.isEmpty()) { + Accessibility::setName(mGenerateButton, txt); + } +} + +void PinEntryDialog::setGenpinTT(const QString &txt) { + if (mGenerateButton) { + mGenerateButton->setToolTip(txt); + } +} + +void PinEntryDialog::setCapsLockHint(const QString &txt) { + mCapsLockHint->setText(txt); +} + +void PinEntryDialog::setFormattedPassphrase( + const PinEntryDialog::FormattedPassphraseOptions &options) { + mFormatPassphrase = options.formatPassphrase; + mFormattedPassphraseHint->setTextFormat(Qt::RichText); + mFormattedPassphraseHint->setText(QLatin1String("<html>") + + options.hint.toHtmlEscaped() + + QLatin1String("</html>")); + Accessibility::setName(mFormattedPassphraseHint, options.hint); + // toggleFormattedPassphrase(); +} + +void PinEntryDialog::setConstraintsOptions(const ConstraintsOptions &options) { + mEnforceConstraints = options.enforce; + mConstraintsHint->setText(options.shortHint); + if (!options.longHint.isEmpty()) { + mConstraintsHint->setToolTip( + QLatin1String("<html>") + + options.longHint.toHtmlEscaped().replace(QLatin1String("\n\n"), + QLatin1String("<br>")) + + QLatin1String("</html>")); + Accessibility::setDescription(mConstraintsHint, options.longHint); + } + mConstraintsErrorTitle = options.errorTitle; + + mConstraintsHint->setVisible(mEnforceConstraints && + !options.shortHint.isEmpty()); +} + +void PinEntryDialog::toggleFormattedPassphrase() { + const bool enableFormatting = + mFormatPassphrase && _edit->echoMode() == QLineEdit::Normal; + _edit->setFormattedPassphrase(enableFormatting); + if (mRepeat) { + mRepeat->setFormattedPassphrase(enableFormatting); + const bool hintAboutToBeHidden = + mFormattedPassphraseHint->isVisible() && !enableFormatting; + if (hintAboutToBeHidden) { + // set hint spacer to current height of hint label before hiding the hint + mFormattedPassphraseHintSpacer->setMinimumHeight( + mFormattedPassphraseHint->height()); + mFormattedPassphraseHintSpacer->setVisible(true); + } else if (enableFormatting) { + mFormattedPassphraseHintSpacer->setVisible(false); + } + mFormattedPassphraseHint->setVisible(enableFormatting); + } +} + +void PinEntryDialog::onBackspace() { + cancelTimeout(); + + if (_disable_echo_allowed) { + _edit->setEchoMode(QLineEdit::NoEcho); + if (mRepeat) { + mRepeat->setEchoMode(QLineEdit::NoEcho); + } + } +} + +void PinEntryDialog::updateQuality(const QString &txt) { + int length; + int percent; + QPalette pal; + + _disable_echo_allowed = false; + + if (!_have_quality_bar || !_pinentry_info) { + return; + } + const QByteArray utf8_pin = txt.toUtf8(); + const char *pin = utf8_pin.constData(); + length = strlen(pin); + percent = length ? pinentry_inq_quality(_pinentry_info, pin, length) : 0; + if (!length) { + _quality_bar->reset(); + } else { + pal = _quality_bar->palette(); + if (percent < 0) { + pal.setColor(QPalette::Highlight, QColor("red")); + percent = -percent; + } else { + pal.setColor(QPalette::Highlight, QColor("green")); + } + _quality_bar->setPalette(pal); + _quality_bar->setValue(percent); + } +} + +void PinEntryDialog::setPinentryInfo(pinentry_t peinfo) { + _pinentry_info = peinfo; +} + +void PinEntryDialog::focusChanged(QWidget *old, QWidget *now) { + // Grab keyboard. It might be a little weird to do it here, but it works! + // Previously this code was in showEvent, but that did not work in Qt4. + if (!_pinentry_info || _pinentry_info->grab) { + if (_grabbed && old && (old == _edit || old == mRepeat)) { + old->releaseKeyboard(); + _grabbed = false; + } + if (!_grabbed && now && (now == _edit || now == mRepeat)) { + now->grabKeyboard(); + _grabbed = true; + } + } +} + +void PinEntryDialog::textChanged(const QString &text) { + Q_UNUSED(text); + + cancelTimeout(); + + if (mVisiActionEdit && sender() == _edit) { + mVisiActionEdit->setVisible(!_edit->pin().isEmpty()); + } + if (mGenerateButton) { + mGenerateButton->setVisible(_edit->pin().isEmpty() +#ifndef QT_NO_ACCESSIBILITY + && !mGenerateButton->accessibleName().isEmpty() +#endif + ); + } +} + +void PinEntryDialog::generatePin() { + unique_malloced_ptr<char> pin{pinentry_inq_genpin(_pinentry_info)}; + if (pin) { + if (_edit->echoMode() == QLineEdit::Password) { + if (mVisiActionEdit) { + mVisiActionEdit->trigger(); + } + if (mVisiCB) { + mVisiCB->setChecked(true); + } + } + const auto pinStr = QString::fromUtf8(pin.get()); + _edit->setPin(pinStr); + mRepeat->setPin(pinStr); + // explicitly focus the first input field and select the generated password + _edit->setFocus(); + _edit->selectAll(); + } +} + +void PinEntryDialog::toggleVisibility() { + if (sender() != mVisiCB) { + if (_edit->echoMode() == QLineEdit::Password) { + if (mVisiActionEdit) { + mVisiActionEdit->setIcon(QIcon(QLatin1String(":/hint.svg"))); + mVisiActionEdit->setToolTip(mHideTT); + } + _edit->setEchoMode(QLineEdit::Normal); + if (mRepeat) { + mRepeat->setEchoMode(QLineEdit::Normal); + } + } else { + if (mVisiActionEdit) { + mVisiActionEdit->setIcon(QIcon(QLatin1String(":/visibility.svg"))); + mVisiActionEdit->setToolTip(mVisibilityTT); + } + _edit->setEchoMode(QLineEdit::Password); + if (mRepeat) { + mRepeat->setEchoMode(QLineEdit::Password); + } + } + } else { + if (mVisiCB->isChecked()) { + if (mRepeat) { + mRepeat->setEchoMode(QLineEdit::Normal); + } + _edit->setEchoMode(QLineEdit::Normal); + } else { + if (mRepeat) { + mRepeat->setEchoMode(QLineEdit::Password); + } + _edit->setEchoMode(QLineEdit::Password); + } + } + toggleFormattedPassphrase(); +} + +QString PinEntryDialog::repeatedPin() const { + if (mRepeat) { + return mRepeat->pin(); + } + return QString(); +} + +bool PinEntryDialog::timedOut() const { return _timed_out; } + +void PinEntryDialog::setRepeatErrorText(const QString &err) { + if (mRepeatError) { + mRepeatError->setText(err); + } +} + +void PinEntryDialog::cancelTimeout() { + if (_timer) { + _timer->stop(); + } +} + +void PinEntryDialog::checkCapsLock() { + const auto state = capsLockState(); + if (state != LockState::Unknown) { + mCapsLockHint->setVisible(state == LockState::On); + } +} + +void PinEntryDialog::onAccept() { + cancelTimeout(); + + if (mRepeat && mRepeat->pin() != _edit->pin()) { +#ifndef QT_NO_ACCESSIBILITY + if (QAccessible::isActive()) { + QMessageBox::information(this, mRepeatError->text(), + mRepeatError->text()); + } else +#endif + { + mRepeatError->setVisible(true); + } + return; + } + + const auto result = checkConstraints(); + if (result != PassphraseNotOk) { + accept(); + } +} + +#ifndef QT_NO_ACCESSIBILITY +void PinEntryDialog::accessibilityActiveChanged(bool active) { + // Allow text labels to get focus if accessibility is active + const auto focusPolicy = active ? Qt::StrongFocus : Qt::ClickFocus; + _error->setFocusPolicy(focusPolicy); + _desc->setFocusPolicy(focusPolicy); + mCapsLockHint->setFocusPolicy(focusPolicy); + mConstraintsHint->setFocusPolicy(focusPolicy); + mFormattedPassphraseHint->setFocusPolicy(focusPolicy); + if (mRepeatError) { + mRepeatError->setFocusPolicy(focusPolicy); + } +} +#endif + +PinEntryDialog::PassphraseCheckResult PinEntryDialog::checkConstraints() { + if (!mEnforceConstraints) { + return PassphraseNotChecked; + } + + const auto passphrase = _edit->pin().toUtf8(); + unique_malloced_ptr<char> error{pinentry_inq_checkpin( + _pinentry_info, passphrase.constData(), passphrase.size())}; + + if (!error) { + return PassphraseOk; + } + + const auto messageLines = + QString::fromUtf8(QByteArray::fromPercentEncoding(error.get())) + .split(QChar{'\n'}); + if (messageLines.isEmpty()) { + // shouldn't happen because pinentry_inq_checkpin() either returns NULL or a + // non-empty string + return PassphraseOk; + } + const auto firstLine = messageLines.first(); + const auto indexOfFirstNonEmptyAdditionalLine = + messageLines.indexOf(QRegularExpression{QStringLiteral(".*\\S.*")}, 1); + const auto additionalLines = + indexOfFirstNonEmptyAdditionalLine > 0 + ? messageLines.mid(indexOfFirstNonEmptyAdditionalLine) + .join(QChar{'\n'}) + : QString{}; + QMessageBox messageBox{this}; + messageBox.setIcon(QMessageBox::Information); + messageBox.setWindowTitle(mConstraintsErrorTitle); + messageBox.setText(firstLine); + messageBox.setInformativeText(additionalLines); + messageBox.setStandardButtons(QMessageBox::Ok); + messageBox.exec(); + return PassphraseNotOk; +} diff --git a/src/pinentry/pinentrydialog.h b/src/pinentry/pinentrydialog.h new file mode 100644 index 00000000..97a90478 --- /dev/null +++ b/src/pinentry/pinentrydialog.h @@ -0,0 +1,180 @@ +/* pinentrydialog.h - A (not yet) secure Qt 4 dialog for PIN entry. + * Copyright (C) 2002, 2008 Klarälvdalens Datakonsult AB (KDAB) + * Copyright 2007 Ingo Klöcker + * Copyright 2016 Intevation GmbH + * Copyright (C) 2021, 2022 g10 Code GmbH + * + * Written by Steffen Hansen <[email protected]>. + * Modified by Andre Heinecke <[email protected]> + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRYDIALOG_H__ +#define __PINENTRYDIALOG_H__ + +#include <QAccessible> +#include <QDialog> +#include <QStyle> +#include <QTimer> + +#include "pinentry.h" + +class QIcon; +class QLabel; +class QPushButton; +class QLineEdit; +class PinLineEdit; +class QString; +class QProgressBar; +class QCheckBox; +class QAction; + +QPixmap applicationIconPixmap(const QIcon &overlayIcon = {}); + +void raiseWindow(QWidget *w); + +class PinEntryDialog : public QDialog +#ifndef QT_NO_ACCESSIBILITY + , + public QAccessible::ActivationObserver +#endif +{ + Q_OBJECT + + Q_PROPERTY(QString description READ description WRITE setDescription) + Q_PROPERTY(QString error READ error WRITE setError) + Q_PROPERTY(QString pin READ pin WRITE setPin) + Q_PROPERTY(QString prompt READ prompt WRITE setPrompt) + public: + struct FormattedPassphraseOptions { + bool formatPassphrase; + QString hint; + }; + struct ConstraintsOptions { + bool enforce; + QString shortHint; + QString longHint; + QString errorTitle; + }; + + explicit PinEntryDialog(QWidget *parent = 0, const char *name = 0, + int timeout = 0, bool modal = false, + bool enable_quality_bar = false, + const QString &repeatString = QString(), + const QString &visibiltyTT = QString(), + const QString &hideTT = QString()); + ~PinEntryDialog() override; + + void setDescription(const QString &); + QString description() const; + + void setError(const QString &); + QString error() const; + + void setPin(const QString &); + QString pin() const; + + QString repeatedPin() const; + void setRepeatErrorText(const QString &); + + void setPrompt(const QString &); + QString prompt() const; + + void setOkText(const QString &); + void setCancelText(const QString &); + + void setQualityBar(const QString &); + void setQualityBarTT(const QString &); + + void setGenpinLabel(const QString &); + void setGenpinTT(const QString &); + + void setCapsLockHint(const QString &); + + void setFormattedPassphrase(const FormattedPassphraseOptions &options); + + void setConstraintsOptions(const ConstraintsOptions &options); + + void setPinentryInfo(pinentry_t); + + bool timedOut() const; + + protected Q_SLOTS: + void updateQuality(const QString &); + void slotTimeout(); + void textChanged(const QString &); + void focusChanged(QWidget *old, QWidget *now); + void toggleVisibility(); + void onBackspace(); + void generatePin(); + void toggleFormattedPassphrase(); + + protected: + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + void showEvent(QShowEvent *event) override; + + private Q_SLOTS: + void cancelTimeout(); + void checkCapsLock(); + void onAccept(); + + private: +#ifndef QT_NO_ACCESSIBILITY + void accessibilityActiveChanged(bool active) override; +#endif + + enum PassphraseCheckResult { + PassphraseNotChecked = -1, + PassphraseNotOk = 0, + PassphraseOk + }; + PassphraseCheckResult checkConstraints(); + + private: + QLabel *_icon = nullptr; + QLabel *_desc = nullptr; + QLabel *_error = nullptr; + QLabel *_prompt = nullptr; + QLabel *_quality_bar_label = nullptr; + QProgressBar *_quality_bar = nullptr; + PinLineEdit *_edit = nullptr; + PinLineEdit *mRepeat = nullptr; + QLabel *mRepeatError = nullptr; + QPushButton *_ok = nullptr; + QPushButton *_cancel = nullptr; + bool _grabbed = false; + bool _have_quality_bar = false; + bool _timed_out = false; + bool _disable_echo_allowed = true; + bool mEnforceConstraints = false; + bool mFormatPassphrase = false; + pinentry_t _pinentry_info = nullptr; + QTimer *_timer = nullptr; + QString mVisibilityTT; + QString mHideTT; + QAction *mVisiActionEdit = nullptr; + QPushButton *mGenerateButton = nullptr; + QCheckBox *mVisiCB = nullptr; + QLabel *mFormattedPassphraseHint = nullptr; + QLabel *mFormattedPassphraseHintSpacer = nullptr; + QLabel *mCapsLockHint = nullptr; + QLabel *mConstraintsHint = nullptr; + QString mConstraintsErrorTitle; +}; + +#endif // __PINENTRYDIALOG_H__ diff --git a/src/pinentry/pinlineedit.cpp b/src/pinentry/pinlineedit.cpp new file mode 100644 index 00000000..9d172b56 --- /dev/null +++ b/src/pinentry/pinlineedit.cpp @@ -0,0 +1,204 @@ +/* pinlineedit.cpp - Modified QLineEdit widget. + * Copyright (C) 2018 Damien Goutte-Gattat + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "pinlineedit.h" + +#include <QClipboard> +#include <QGuiApplication> +#include <QKeyEvent> + +static const int FormattedPassphraseGroupSize = 5; +static const QChar FormattedPassphraseSeparator = QChar::Nbsp; + +namespace { +struct Selection { + bool empty() const { return start < 0 || start >= end; } + int length() const { return empty() ? 0 : end - start; } + + int start; + int end; +}; +} // namespace + +class PinLineEdit::Private { + PinLineEdit *const q; + + public: + Private(PinLineEdit *q) : q{q} {} + + QString formatted(QString text) const { + const int dashCount = text.size() / FormattedPassphraseGroupSize; + text.reserve(text.size() + dashCount); + for (int i = FormattedPassphraseGroupSize; i < text.size(); + i += FormattedPassphraseGroupSize + 1) { + text.insert(i, FormattedPassphraseSeparator); + } + return text; + } + + Selection formattedSelection(Selection selection) const { + if (selection.empty()) { + return selection; + } + return {selection.start + selection.start / FormattedPassphraseGroupSize, + selection.end + (selection.end - 1) / FormattedPassphraseGroupSize}; + } + + QString unformatted(QString text) const { + for (int i = FormattedPassphraseGroupSize; i < text.size(); + i += FormattedPassphraseGroupSize) { + text.remove(i, 1); + } + return text; + } + + Selection unformattedSelection(Selection selection) const { + if (selection.empty()) { + return selection; + } + return { + selection.start - selection.start / (FormattedPassphraseGroupSize + 1), + selection.end - selection.end / (FormattedPassphraseGroupSize + 1)}; + } + + void copyToClipboard() { + if (q->echoMode() != QLineEdit::Normal) { + return; + } + + QString text = q->selectedText(); + if (mFormattedPassphrase) { + text.remove(FormattedPassphraseSeparator); + } + if (!text.isEmpty()) { + QGuiApplication::clipboard()->setText(text); + } + } + + int selectionEnd() { +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) + return q->selectionEnd(); +#else + return q->selectionStart() + q->selectedText().size(); +#endif + } + + public: + bool mFormattedPassphrase = false; +}; + +PinLineEdit::PinLineEdit(QWidget *parent) + : QLineEdit(parent), d{new Private{this}} { + connect(this, SIGNAL(textEdited(QString)), this, SLOT(textEdited())); +} + +PinLineEdit::~PinLineEdit() = default; + +void PinLineEdit::setFormattedPassphrase(bool on) { + if (on == d->mFormattedPassphrase) { + return; + } + d->mFormattedPassphrase = on; + Selection selection{selectionStart(), d->selectionEnd()}; + if (d->mFormattedPassphrase) { + setText(d->formatted(text())); + selection = d->formattedSelection(selection); + } else { + setText(d->unformatted(text())); + selection = d->unformattedSelection(selection); + } + if (!selection.empty()) { + setSelection(selection.start, selection.length()); + } +} + +void PinLineEdit::copy() const { d->copyToClipboard(); } + +void PinLineEdit::cut() { + if (hasSelectedText()) { + copy(); + del(); + } +} + +void PinLineEdit::setPin(const QString &pin) { + setText(d->mFormattedPassphrase ? d->formatted(pin) : pin); +} + +QString PinLineEdit::pin() const { + if (d->mFormattedPassphrase) { + return d->unformatted(text()); + } else { + return text(); + } +} + +void PinLineEdit::keyPressEvent(QKeyEvent *e) { + if (e == QKeySequence::Copy) { + copy(); + return; + } else if (e == QKeySequence::Cut) { + if (!isReadOnly() && hasSelectedText()) { + copy(); + del(); + } + return; + } else if (e == QKeySequence::DeleteEndOfLine) { + if (!isReadOnly()) { + setSelection(cursorPosition(), text().size()); + copy(); + del(); + } + return; + } else if (e == QKeySequence::DeleteCompleteLine) { + if (!isReadOnly()) { + setSelection(0, text().size()); + copy(); + del(); + } + return; + } + + QLineEdit::keyPressEvent(e); + + if (e->key() == Qt::Key::Key_Backspace) { + emit backspacePressed(); + } +} + +void PinLineEdit::textEdited() { + if (!d->mFormattedPassphrase) { + return; + } + auto currentText = text(); + // first calculate the cursor position in the reformatted text; the cursor + // is put left of the separators, so that backspace works as expected + auto cursorPos = cursorPosition(); + cursorPos -= QStringView{currentText}.left(cursorPos).count( + FormattedPassphraseSeparator); + cursorPos += std::max(cursorPos - 1, 0) / FormattedPassphraseGroupSize; + // then reformat the text + currentText.remove(FormattedPassphraseSeparator); + currentText = d->formatted(currentText); + // finally, set reformatted text and updated cursor position + setText(currentText); + setCursorPosition(cursorPos); +} diff --git a/src/pinentry/pinlineedit.h b/src/pinentry/pinlineedit.h new file mode 100644 index 00000000..e67ced83 --- /dev/null +++ b/src/pinentry/pinlineedit.h @@ -0,0 +1,63 @@ +/* pinlineedit.h - Modified QLineEdit widget. + * Copyright (C) 2018 Damien Goutte-Gattat + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef _PINLINEEDIT_H_ +#define _PINLINEEDIT_H_ + +#include <QLineEdit> + +#include <memory> + +class PinLineEdit : public QLineEdit +{ + Q_OBJECT + +public: + explicit PinLineEdit(QWidget *parent = nullptr); + ~PinLineEdit() override; + + void setPin(const QString &pin); + QString pin() const; + +public Q_SLOTS: + void setFormattedPassphrase(bool on); + void copy() const; + void cut(); + +Q_SIGNALS: + void backspacePressed(); + +protected: + void keyPressEvent(QKeyEvent *) override; + +private: + using QLineEdit::setText; + using QLineEdit::text; + +private Q_SLOTS: + void textEdited(); + +private: + class Private; + std::unique_ptr<Private> d; +}; + +#endif // _PINLINEEDIT_H_ diff --git a/src/pinentry/qti18n.cpp b/src/pinentry/qti18n.cpp new file mode 100644 index 00000000..8e6bb591 --- /dev/null +++ b/src/pinentry/qti18n.cpp @@ -0,0 +1,93 @@ +/* qti18n.cpp - Load qt translations for pinentry. + * Copyright 2021 g10 Code GmbH + * SPDX-FileCopyrightText: 2015 Lukáš Tinkl <[email protected]> + * SPDX-FileCopyrightText: 2021 Ingo Klöcker <[email protected]> + * + * Copied from k18n under the terms of LGPLv2 or later. + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <QDebug> +#include <QCoreApplication> +#include <QLibraryInfo> +#include <QLocale> +#include <QTranslator> + +#include <memory> + +static bool loadCatalog(const QString &catalog, const QLocale &locale) +{ + auto translator = new QTranslator(QCoreApplication::instance()); + + if (!translator->load(locale, catalog, QString(), QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { + qDebug() << "Loading the" << catalog << "catalog failed for locale" << locale; + delete translator; + return false; + } + QCoreApplication::instance()->installTranslator(translator); + return true; +} + +static bool loadCatalog(const QString &catalog, const QLocale &locale, const QLocale &fallbackLocale) +{ + // try to load the catalog for locale + if (loadCatalog(catalog, locale)) { + return true; + } + // if this fails, then try the fallback locale (if it's different from locale) + if (fallbackLocale != locale) { + return loadCatalog(catalog, fallbackLocale); + } + return false; +} + +// load global Qt translation, needed in KDE e.g. by lots of builtin dialogs (QColorDialog, QFontDialog) that we use +static void loadTranslation(const QString &localeName, const QString &fallbackLocaleName) +{ + const QLocale locale{localeName}; + const QLocale fallbackLocale{fallbackLocaleName}; + // first, try to load the qt_ meta catalog + if (loadCatalog(QStringLiteral("qt_"), locale, fallbackLocale)) { + return; + } + // if loading the meta catalog failed, then try loading the four catalogs + // it depends on, i.e. qtbase, qtscript, qtmultimedia, qtxmlpatterns, separately + const auto catalogs = { + QStringLiteral("qtbase_"), + /* QStringLiteral("qtscript_"), + QStringLiteral("qtmultimedia_"), + QStringLiteral("qtxmlpatterns_"), */ + }; + for (const auto &catalog : catalogs) { + loadCatalog(catalog, locale, fallbackLocale); + } +} + +static void load() +{ + // The way Qt translation system handles plural forms makes it necessary to + // have a translation file which contains only plural forms for `en`. That's + // why we load the `en` translation unconditionally, then load the + // translation for the current locale to overload it. + loadCatalog(QStringLiteral("qt_"), QLocale{QStringLiteral("en")}); + + const QLocale locale = QLocale::system(); + if (locale.name() != QStringLiteral("en")) { + loadTranslation(locale.name(), locale.bcp47Name()); + } +} + +Q_COREAPP_STARTUP_FUNCTION(load) diff --git a/src/pinentry/secmem++.h b/src/pinentry/secmem++.h new file mode 100644 index 00000000..116da880 --- /dev/null +++ b/src/pinentry/secmem++.h @@ -0,0 +1,91 @@ +/* STL allocator for secmem + * Copyright (C) 2008 Marc Mutz <[email protected]> + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __SECMEM_SECMEMPP_H__ +#define __SECMEM_SECMEMPP_H__ + +#include "../secmem/secmem.h" +#include <cstddef> + +namespace secmem { + + template <typename T> + class alloc { + public: + // type definitions: + typedef size_t size_type; + typedef ptrdiff_t difference_type; + typedef T* pointer; + typedef const T* const_pointer; + typedef T& reference; + typedef const T& const_reference; + typedef T value_type; + + // rebind + template <typename U> + struct rebind { + typedef alloc<U> other; + }; + + // address + pointer address( reference value ) const { + return &value; + } + const_pointer address( const_reference value ) const { + return &value; + } + + // (trivial) ctors and dtors + alloc() {} + alloc( const alloc & ) {} + template <typename U> alloc( const alloc<U> & ) {} + // copy ctor is ok + ~alloc() {} + + // de/allocation + size_type max_size() const { + return secmem_get_max_size(); + } + + pointer allocate( size_type n, void * =0 ) { + return static_cast<pointer>( secmem_malloc( n * sizeof(T) ) ); + } + + void deallocate( pointer p, size_type ) { + secmem_free( p ); + } + + // de/construct + void construct( pointer p, const T & value ) { + void * loc = p; + new (loc)T(value); + } + void destruct( pointer p ) { + p->~T(); + } + }; + + // equality comparison + template <typename T1,typename T2> + bool operator==( const alloc<T1> &, const alloc<T2> & ) { return true; } + template <typename T1, typename T2> + bool operator!=( const alloc<T1> &, const alloc<T2> & ) { return false; } + +} + +#endif /* __SECMEM_SECMEMPP_H__ */ diff --git a/src/pinentry/secmem.cpp b/src/pinentry/secmem.cpp new file mode 100644 index 00000000..cb4eeb4f --- /dev/null +++ b/src/pinentry/secmem.cpp @@ -0,0 +1,375 @@ +/* secmem.c - memory allocation from a secure heap + * Copyright (C) 1998, 1999, 2003 Free Software Foundation, Inc. + * Copyright (C) 2015 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG 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 2 of the License, or + * (at your option) any later version. + * + * GnuPG 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#ifndef WINDOWS +#include <errno.h> +#endif +#include <stdarg.h> +#include <unistd.h> +#if defined(HAVE_MLOCK) || defined(HAVE_MMAP) +#include <fcntl.h> +#include <sys/mman.h> +#include <sys/types.h> +#endif +#include <string.h> + +#include "secmem.h" + +#ifdef ORIGINAL_GPG_VERSION +#include "types.h" +#include "util.h" +#else /* ORIGINAL_GPG_VERSION */ + +#include "secmem_util.h" + +typedef union { + int a; + short b; + char c[1]; + long d; +#ifdef HAVE_U64_TYPE + u64 e; +#endif + float f; + double g; +} PROPERLY_ALIGNED_TYPE; + +#define log_error log_info +#define log_bug log_fatal + +void log_info(char *template_, ...) { + va_list args; + + va_start(args, template_); + vfprintf(stderr, template_, args); + va_end(args); +} + +void log_fatal(char *template_, ...) { + va_list args; + + va_start(args, template_); + vfprintf(stderr, template_, args); + va_end(args); + exit(EXIT_FAILURE); +} + +#endif /* ORIGINAL_GPG_VERSION */ + +#if defined(MAP_ANON) && !defined(MAP_ANONYMOUS) +#define MAP_ANONYMOUS MAP_ANON +#endif + +#define DEFAULT_POOLSIZE 16384 + +typedef struct memblock_struct MEMBLOCK; +struct memblock_struct { + unsigned size; + union { + MEMBLOCK *next; + PROPERLY_ALIGNED_TYPE aligned; + } u; +}; + +static void *pool; +static volatile int pool_okay; /* may be checked in an atexit function */ +#if HAVE_MMAP +static int pool_is_mmapped; +#endif +static size_t poolsize; /* allocated length */ +static size_t poollen; /* used length */ +static MEMBLOCK *unused_blocks; +static unsigned max_alloced; +static unsigned cur_alloced; +static unsigned max_blocks; +static unsigned cur_blocks; +static int disable_secmem; +static int show_warning; +static int no_warning; +static int suspend_warning; + +static void print_warn(void) { + if (!no_warning) log_info("Warning: using insecure memory!\n"); +} + +static void lock_pool(void *p, size_t n) { +#if defined(HAVE_MLOCK) + uid_t uid; + int err; + + uid = getuid(); + +#ifdef HAVE_BROKEN_MLOCK + if (uid) { + errno = EPERM; + err = -1; + } else { + err = mlock(p, n); + } +#else + err = mlock(p, n); +#endif + + if (uid && !geteuid()) { + if (setuid(uid) || getuid() != geteuid()) + log_fatal("failed to reset uid: %s\n", strerror(errno)); + } + + if (err) { + if (errno != EPERM +#ifdef EAGAIN /* OpenBSD returns this */ + && errno != EAGAIN +#endif + ) + log_error("can't lock memory: %s\n", strerror(errno)); + show_warning = 1; + } + +#else + (void)p; + (void)n; + log_info("Please note that you don't have secure memory on this system\n"); +#endif +} + +static void init_pool(size_t n) { +#if HAVE_MMAP + size_t pgsize; +#endif + + poolsize = n; + + if (disable_secmem) log_bug("secure memory is disabled"); + +#if HAVE_MMAP +#ifdef HAVE_GETPAGESIZE + pgsize = getpagesize(); +#else + pgsize = 4096; +#endif + + poolsize = (poolsize + pgsize - 1) & ~(pgsize - 1); +#ifdef MAP_ANONYMOUS + pool = mmap(0, poolsize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); +#else /* map /dev/zero instead */ + { + int fd; + + fd = open("/dev/zero", O_RDWR); + if (fd == -1) { + log_error("can't open /dev/zero: %s\n", strerror(errno)); + pool = (void *)-1; + } else { + pool = mmap(0, poolsize, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); + close(fd); + } + } +#endif + if (pool == (void *)-1) + log_info("can't mmap pool of %u bytes: %s - using malloc\n", + (unsigned)poolsize, strerror(errno)); + else { + pool_is_mmapped = 1; + pool_okay = 1; + } + +#endif + if (!pool_okay) { + pool = malloc(poolsize); + if (!pool) + log_fatal("can't allocate memory pool of %u bytes\n", (unsigned)poolsize); + else + pool_okay = 1; + } + lock_pool(pool, poolsize); + poollen = 0; +} + +/* concatenate unused blocks */ +static void compress_pool(void) { /* fixme: we really should do this */ +} + +void secmem_set_flags(unsigned flags) { + int was_susp = suspend_warning; + + no_warning = flags & 1; + suspend_warning = flags & 2; + + /* and now issue the warning if it is not longer suspended */ + if (was_susp && !suspend_warning && show_warning) { + show_warning = 0; + print_warn(); + } +} + +unsigned secmem_get_flags(void) { + unsigned flags; + + flags = no_warning ? 1 : 0; + flags |= suspend_warning ? 2 : 0; + return flags; +} + +void secmem_init(size_t n) { + if (!n) { +#if !defined(HAVE_DOSISH_SYSTEM) + uid_t uid; + + disable_secmem = 1; + uid = getuid(); + if (uid != geteuid()) { + if (setuid(uid) || getuid() != geteuid()) + log_fatal("failed to drop setuid\n"); + } +#endif + } else { + if (n < DEFAULT_POOLSIZE) n = DEFAULT_POOLSIZE; + if (!pool_okay) + init_pool(n); + else + log_error("Oops, secure memory pool already initialized\n"); + } +} + +void *secmem_malloc(size_t size) { + MEMBLOCK *mb, *mb2; + int compressed = 0; + + if (!pool_okay) { + log_info("operation is not possible without initialized secure memory\n"); + log_info("(you may have used the wrong program for this task)\n"); + exit(2); + } + if (show_warning && !suspend_warning) { + show_warning = 0; + print_warn(); + } + + /* blocks are always a multiple of 32 */ + size += sizeof(MEMBLOCK); + size = ((size + 31) / 32) * 32; + +retry: + /* try to get it from the used blocks */ + for (mb = unused_blocks, mb2 = NULL; mb; mb2 = mb, mb = mb->u.next) + if (mb->size >= size) { + if (mb2) + mb2->u.next = mb->u.next; + else + unused_blocks = mb->u.next; + goto leave; + } + /* allocate a new block */ + if ((poollen + size <= poolsize)) { + mb = (MEMBLOCK *)((char *)pool + poollen); + poollen += size; + mb->size = size; + } else if (!compressed) { + compressed = 1; + compress_pool(); + goto retry; + } else + return NULL; + +leave: + cur_alloced += mb->size; + cur_blocks++; + if (cur_alloced > max_alloced) max_alloced = cur_alloced; + if (cur_blocks > max_blocks) max_blocks = cur_blocks; + + memset(&mb->u.aligned.c, 0, + size - (size_t) & ((struct memblock_struct *)0)->u.aligned.c); + + return &mb->u.aligned.c; +} + +void *secmem_realloc(void *p, size_t newsize) { + MEMBLOCK *mb; + size_t size; + void *a; + + if (!p) return secmem_malloc(newsize); + + mb = (MEMBLOCK *)(void *)((char *)p - offsetof(MEMBLOCK, u.aligned.c)); + + size = mb->size; + if (newsize < size) return p; /* it is easier not to shrink the memory */ + a = secmem_malloc(newsize); + memcpy(a, p, size); + memset((char *)a + size, 0, newsize - size); + secmem_free(p); + return a; +} + +void secmem_free(void *a) { + MEMBLOCK *mb; + size_t size; + + if (!a) return; + + mb = (MEMBLOCK *)(void *)((char *)a - offsetof(MEMBLOCK, u.aligned.c)); + size = mb->size; + /* This does not make much sense: probably this memory is held in the + * cache. We do it anyway: */ + wipememory2(mb, 0xff, size); + wipememory2(mb, 0xaa, size); + wipememory2(mb, 0x55, size); + wipememory2(mb, 0x00, size); + mb->size = size; + mb->u.next = unused_blocks; + unused_blocks = mb; + cur_blocks--; + cur_alloced -= size; +} + +int m_is_secure(const void *p) { + return p >= pool && p < (void *)((char *)pool + poolsize); +} + +void secmem_term(void) { + if (!pool_okay) return; + + wipememory2(pool, 0xff, poolsize); + wipememory2(pool, 0xaa, poolsize); + wipememory2(pool, 0x55, poolsize); + wipememory2(pool, 0x00, poolsize); +#if HAVE_MMAP + if (pool_is_mmapped) munmap(pool, poolsize); +#endif + pool = NULL; + pool_okay = 0; + poolsize = 0; + poollen = 0; + unused_blocks = NULL; +} + +void secmem_dump_stats(void) { + if (disable_secmem) return; + fprintf(stderr, "secmem usage: %u/%u bytes in %u/%u blocks of pool %lu/%lu\n", + cur_alloced, max_alloced, cur_blocks, max_blocks, (ulong)poollen, + (ulong)poolsize); +} + +size_t secmem_get_max_size(void) { return poolsize; } diff --git a/src/pinentry/secmem.h b/src/pinentry/secmem.h new file mode 100644 index 00000000..75fe708b --- /dev/null +++ b/src/pinentry/secmem.h @@ -0,0 +1,55 @@ +/* Quintuple Agent secure memory allocation + * Copyright (C) 1998,1999 Free Software Foundation, Inc. + * Copyright (C) 1999,2000 Robert Bihlmeyer <[email protected]> + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef _MEMORY_H +#define _MEMORY_H + +#include <sys/types.h> + +#ifdef __cplusplus +extern "C" { +#if 0 +} +#endif +#endif + + +/* values for flags, hardcoded in secmem.c */ +#define SECMEM_WARN 0 +#define SECMEM_DONT_WARN 1 +#define SECMEM_SUSPEND_WARN 2 + +void secmem_init( size_t npool ); +void secmem_term( void ); +void *secmem_malloc( size_t size ); +void *secmem_realloc( void *a, size_t newsize ); +void secmem_free( void *a ); +int m_is_secure( const void *p ); +void secmem_dump_stats(void); +void secmem_set_flags( unsigned flags ); +unsigned secmem_get_flags(void); +size_t secmem_get_max_size (void); + +#if 0 +{ +#endif +#ifdef __cplusplus +} +#endif +#endif /* _MEMORY_H */ diff --git a/src/pinentry/secmem_util.h b/src/pinentry/secmem_util.h new file mode 100644 index 00000000..3c8ffb8a --- /dev/null +++ b/src/pinentry/secmem_util.h @@ -0,0 +1,65 @@ +/* Quintuple Agent utilities + * Copyright (C) 1999 Robert Bihlmeyer <[email protected]> + * Copyright (C) 2003 g10 Code GmbH + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef _UTIL_H +#define _UTIL_H + +#include <sys/types.h> + +#ifndef HAVE_TYPE_BYTE +# undef byte +# if !(defined(_WIN32) && defined(cbNDRContext)) + /* Windows typedefs byte in the rpc headers. Avoid warning about + double definition. */ + typedef unsigned char byte; +# endif +# define HAVE_TYPE_BYTE +#endif + +#ifndef HAVE_TYPE_ULONG +# undef ulong + typedef unsigned long ulong; +# define HAVE_TYPE_ULONG +#endif + + +ssize_t xwrite(int, const void *, size_t); /* write until finished */ +int debugmsg(const char *, ...); /* output a debug message if debugging==on */ +void drop_privs(void); /* finally drop privileges */ + + +/* To avoid that a compiler optimizes certain memset calls away, these + macros may be used instead. */ +#define wipememory2(_ptr,_set,_len) do { \ + volatile char *_vptr=(volatile char *)(_ptr); \ + size_t _vlen=(_len); \ + while(_vlen) { *_vptr=(_set); _vptr++; _vlen--; } \ + } while(0) +#define wipememory(_ptr,_len) wipememory2(_ptr,0,_len) +#define wipe(_ptr,_len) wipememory2(_ptr,0,_len) + + + + +#define xtoi_1(p) (*(p) <= '9'? (*(p)- '0'): \ + *(p) <= 'F'? (*(p)-'A'+10):(*(p)-'a'+10)) +#define xtoi_2(p) ((xtoi_1(p) * 16) + xtoi_1((p)+1)) + + +#endif diff --git a/src/pinentry/util.cpp b/src/pinentry/util.cpp new file mode 100644 index 00000000..f1bac4ba --- /dev/null +++ b/src/pinentry/util.cpp @@ -0,0 +1,116 @@ +/* Quintuple Agent + * Copyright (C) 1999 Robert Bihlmeyer <[email protected]> + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#define _GNU_SOURCE 1 + +#include <unistd.h> +#ifndef WINDOWS +# include <errno.h> +#endif +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +#include "util.h" + +#ifndef HAVE_DOSISH_SYSTEM +static int uid_set = 0; +static uid_t real_uid, file_uid; +#endif /*!HAVE_DOSISH_SYSTEM*/ + +/* Write DATA of size BYTES to FD, until all is written or an error + occurs. */ +ssize_t +xwrite(int fd, const void *data, size_t bytes) +{ + char *ptr; + size_t todo; + ssize_t written = 0; + + for (ptr = (char *)data, todo = bytes; todo; ptr += written, todo -= written) + { + do + written = write (fd, ptr, todo); + while ( +#ifdef WINDOWS + 0 +#else + written == -1 && errno == EINTR +#endif + ); + if (written < 0) + break; + } + return written; +} + +#if 0 +extern int debug; + +int +debugmsg(const char *fmt, ...) +{ + va_list va; + int ret; + + if (debug) { + va_start(va, fmt); + fprintf(stderr, "\e[4m"); + ret = vfprintf(stderr, fmt, va); + fprintf(stderr, "\e[24m"); + va_end(va); + return ret; + } else + return 0; +} +#endif + +/* initialize uid variables */ +#ifndef HAVE_DOSISH_SYSTEM +static void +init_uids(void) +{ + real_uid = getuid(); + file_uid = geteuid(); + uid_set = 1; +} +#endif + + +/* drop all additional privileges */ +void +drop_privs(void) +{ +#ifndef HAVE_DOSISH_SYSTEM + if (!uid_set) + init_uids(); + if (real_uid != file_uid) { + if (setuid(real_uid) < 0) { + perror("dropping privileges failed"); + exit(EXIT_FAILURE); + } + file_uid = real_uid; + } +#endif +} diff --git a/src/pinentry/util.h b/src/pinentry/util.h new file mode 100644 index 00000000..04f35957 --- /dev/null +++ b/src/pinentry/util.h @@ -0,0 +1,40 @@ +/* util.h - Helper for managing malloced pointers + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker <[email protected]> + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <https://www.gnu.org/licenses/>. + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_UTIL_H__ +#define __PINENTRY_QT_UTIL_H__ + +#include <memory> + +#include <stdlib.h> + +namespace _detail +{ +struct FreeDeleter { + void operator()(void *ptr) const { + free(ptr); + } +}; +} + +template<class T> +using unique_malloced_ptr = std::unique_ptr<T, _detail::FreeDeleter>; + +#endif // __PINENTRY_QT_UTIL_H__ diff --git a/src/ui/GpgFrontendApplication.cpp b/src/ui/GpgFrontendApplication.cpp index 83866c0d..7b079de1 100644 --- a/src/ui/GpgFrontendApplication.cpp +++ b/src/ui/GpgFrontendApplication.cpp @@ -73,6 +73,7 @@ GpgFrontendApplication *GpgFrontendApplication::GetInstance(int argc, } bool GpgFrontendApplication::notify(QObject *receiver, QEvent *event) { +#ifdef RELEASE try { return QApplication::notify(receiver, event); } catch (const std::exception &ex) { @@ -97,6 +98,9 @@ bool GpgFrontendApplication::notify(QObject *receiver, QEvent *event) { "please report this problem if you can.")); } return -1; +#else + return QApplication::notify(receiver, event); +#endif } } // namespace GpgFrontend::UI diff --git a/src/ui/UISignalStation.cpp b/src/ui/UISignalStation.cpp index cae1de85..9370bcf4 100644 --- a/src/ui/UISignalStation.cpp +++ b/src/ui/UISignalStation.cpp @@ -28,15 +28,21 @@ #include "UISignalStation.h" +#include <memory> + +#include "core/function/CoreSignalStation.h" + namespace GpgFrontend::UI { std::unique_ptr<UISignalStation> UISignalStation::instance = nullptr; auto UISignalStation::GetInstance() -> UISignalStation* { if (instance == nullptr) { - instance = std::make_unique<UISignalStation>(); + instance = std::unique_ptr<UISignalStation>(new UISignalStation()); } return instance.get(); } +UISignalStation::UISignalStation() = default; + } // namespace GpgFrontend::UI diff --git a/src/ui/UISignalStation.h b/src/ui/UISignalStation.h index 16c78239..83e7a02c 100644 --- a/src/ui/UISignalStation.h +++ b/src/ui/UISignalStation.h @@ -88,19 +88,22 @@ class UISignalStation : public QObject { * @brief * */ - void SignalUserInputPassphraseDone(QString passparase); + void SignalNeedUserInputPassphrase(); /** * @brief * */ - void SignalNeedUserInputPassphrase(); + void SignalUserInputPassphraseCallback(QByteArray); /** * @brief * */ void SignalRestartApplication(int); + + private: + UISignalStation(); }; } // namespace GpgFrontend::UI diff --git a/src/ui/UserInterfaceUtils.cpp b/src/ui/UserInterfaceUtils.cpp index 97520a25..2e3596f8 100644 --- a/src/ui/UserInterfaceUtils.cpp +++ b/src/ui/UserInterfaceUtils.cpp @@ -164,9 +164,6 @@ CommonUtils::CommonUtils() : QWidget(nullptr) { connect(this, &CommonUtils::SignalKeyDatabaseRefreshDone, UISignalStation::GetInstance(), &UISignalStation::SignalKeyDatabaseRefreshDone); - connect(this, &CommonUtils::SignalUserInputPassphraseDone, - CoreSignalStation::GetInstance(), - &CoreSignalStation::SignalUserInputPassphraseDone); // directly connect to SignalKeyStatusUpdated // to avoid the delay of signal emitting @@ -175,10 +172,6 @@ CommonUtils::CommonUtils() : QWidget(nullptr) { &UISignalStation::SignalKeyDatabaseRefresh, this, &CommonUtils::slot_update_key_status); - connect(CoreSignalStation::GetInstance(), - &CoreSignalStation::SignalNeedUserInputPassphrase, this, - &CommonUtils::slot_popup_passphrase_input_dialog); - connect(this, &CommonUtils::SignalRestartApplication, UISignalStation::GetInstance(), &UISignalStation::SignalRestartApplication); @@ -467,7 +460,7 @@ void CommonUtils::slot_popup_passphrase_input_dialog() { SetTempCacheValue("__key_passphrase", dialog->textValue().toStdString()); // send signal - emit SignalUserInputPassphraseDone(); + // emit SignalUserInputPassphraseDone(); } void CommonUtils::SlotRestartApplication(int code) { diff --git a/src/ui/UserInterfaceUtils.h b/src/ui/UserInterfaceUtils.h index 9f9ef5bb..3746526a 100644 --- a/src/ui/UserInterfaceUtils.h +++ b/src/ui/UserInterfaceUtils.h @@ -192,18 +192,6 @@ class CommonUtils : public QWidget { * @brief * */ - void SignalNeedUserInputPassphrase(); - - /** - * @brief - * - */ - void SignalUserInputPassphraseDone(); - - /** - * @brief - * - */ void SignalRestartApplication(int); public slots: diff --git a/src/ui/dialog/keypair_details/KeyPairOperaTab.cpp b/src/ui/dialog/keypair_details/KeyPairOperaTab.cpp index e5b5ab18..6f1bde9c 100644 --- a/src/ui/dialog/keypair_details/KeyPairOperaTab.cpp +++ b/src/ui/dialog/keypair_details/KeyPairOperaTab.cpp @@ -32,6 +32,7 @@ #include "core/function/GlobalSettingStation.h" #include "core/function/gpg/GpgKeyImportExporter.h" #include "core/function/gpg/GpgKeyOpera.h" +#include "core/typedef/GpgTypedef.h" #include "core/utils/GpgUtils.h" #include "core/utils/IOUtils.h" #include "ui/UISignalStation.h" @@ -343,11 +344,12 @@ void KeyPairOperaTab::slot_gen_revoke_cert() { } void KeyPairOperaTab::slot_modify_password() { - auto err = GpgKeyOpera::GetInstance().ModifyPassword(m_key_); - if (CheckGpgError(err) != GPG_ERR_NO_ERROR) { - QMessageBox::critical(this, _("Not Successful"), - QString(_("Modify password not successfully."))); - } + GpgKeyOpera::GetInstance().ModifyPassword(m_key_, [this](GpgError err) { + if (CheckGpgError(err) != GPG_ERR_NO_ERROR) { + QMessageBox::critical(this, _("Not Successful"), + QString(_("Modify password not successfully."))); + } + }); } void KeyPairOperaTab::slot_modify_tofu_policy() { diff --git a/src/ui/main_window/MainWindow.cpp b/src/ui/main_window/MainWindow.cpp index ce62b376..8cc76d65 100644 --- a/src/ui/main_window/MainWindow.cpp +++ b/src/ui/main_window/MainWindow.cpp @@ -30,6 +30,7 @@ #include "core/GpgModel.h" #include "core/function/CacheManager.h" +#include "core/function/CoreSignalStation.h" #include "core/function/GlobalSettingStation.h" #include "core/function/gpg/GpgAdvancedOperator.h" #include "core/module/ModuleManager.h" @@ -43,6 +44,10 @@ namespace GpgFrontend::UI { MainWindow::MainWindow() : GeneralMainWindow("main_window") { this->setMinimumSize(1200, 700); this->setWindowTitle(qApp->applicationName()); + + connect(CoreSignalStation::GetInstance(), + &CoreSignalStation::SignalNeedUserInputPassphrase, this, + &MainWindow::SlotRaisePinentry); } void MainWindow::Init() noexcept { diff --git a/src/ui/main_window/MainWindow.h b/src/ui/main_window/MainWindow.h index f26a0a36..81975579 100644 --- a/src/ui/main_window/MainWindow.h +++ b/src/ui/main_window/MainWindow.h @@ -152,6 +152,11 @@ class MainWindow : public GeneralMainWindow { */ void SlotSetRestartNeeded(int); + /** + * @details Open a new tab for path + */ + void SlotRaisePinentry(); + private slots: /** diff --git a/src/ui/main_window/MainWindowSlotUI.cpp b/src/ui/main_window/MainWindowSlotUI.cpp index 6c9705ac..d640d337 100644 --- a/src/ui/main_window/MainWindowSlotUI.cpp +++ b/src/ui/main_window/MainWindowSlotUI.cpp @@ -26,9 +26,15 @@ * */ +#include <qnamespace.h> + #include "MainWindow.h" +#include "UISignalStation.h" #include "core/GpgConstants.h" +#include "core/function/CoreSignalStation.h" #include "core/function/GlobalSettingStation.h" +#include "pinentry/pinentrydialog.h" +#include "spdlog/spdlog.h" #include "ui/UserInterfaceUtils.h" #include "ui/dialog/Wizard.h" #include "ui/main_window/KeyMgmt.h" @@ -223,4 +229,52 @@ void MainWindow::SetCryptoMenuStatus( } } +void MainWindow::SlotRaisePinentry() { + auto* pinentry = + new PinEntryDialog(this, 0, 0, true, false, QString(), + QString::fromStdString(_("Show passphrase")), + QString::fromStdString(_("Hide passphrase"))); + + SPDLOG_DEBUG("setting pinetry's arguments"); + + pinentry->setPinentryInfo(new struct pinentry()); + pinentry->setPrompt(QString::fromStdString(_("PIN:"))); + pinentry->setDescription(QString()); + pinentry->setRepeatErrorText( + QString::fromStdString(_("Passphrases do not match"))); + pinentry->setGenpinLabel(QString()); + pinentry->setGenpinTT(QString()); + pinentry->setCapsLockHint(QString::fromStdString(_("Caps Lock is on"))); + pinentry->setFormattedPassphrase({false, QString()}); + pinentry->setConstraintsOptions({false, QString(), QString(), QString()}); + + pinentry->setWindowTitle(_("Pinentry")); + + /* If we reuse the same dialog window. */ + pinentry->setPin(QString()); + + pinentry->setOkText(_("Confirm")); + pinentry->setCancelText(_("Cancel")); + + SPDLOG_DEBUG("pinentry is ready to start"); + + connect(pinentry, &PinEntryDialog::finished, this, [pinentry](int result) { + bool ret = result != 0; + SPDLOG_DEBUG("PinEntryDialog finished, ret: {}", ret); + + if (!ret) { + emit CoreSignalStation::GetInstance()->SignalUserInputPassphraseCallback( + {}); + return -1; + } + + auto pin = pinentry->pin().toUtf8(); + emit CoreSignalStation::GetInstance()->SignalUserInputPassphraseCallback( + pin); + return 0; + }); + + pinentry->open(); +} + } // namespace GpgFrontend::UI |