Index: .gitignore =================================================================== --- .gitignore +++ .gitignore @@ -37,6 +37,8 @@ qt/icons/Makefile qt4/Makefile.in qt4/Makefile +qt5/Makefile.in +qt5/icons/Makefile.in tqt/Makefile.in tqt/Makefile secmem/Makefile.in Index: Makefile.am =================================================================== --- Makefile.am +++ Makefile.am @@ -58,12 +58,18 @@ pinentry_gnome_3 = endif -if BUILD_PINENTRY_QT5 +if BUILD_PINENTRY_QT6 pinentry_qt = qt else pinentry_qt = endif +if BUILD_PINENTRY_QT5 +pinentry_qt5 = qt5 +else +pinentry_qt5 = +endif + if BUILD_PINENTRY_QT4 pinentry_qt4 = qt4 else @@ -102,7 +108,7 @@ SUBDIRS = m4 secmem pinentry ${pinentry_curses} ${pinentry_tty} \ ${pinentry_emacs} ${pinentry_gtk_2} ${pinentry_gnome_3} \ - ${pinentry_qt} ${pinentry_qt4} ${pinentry_tqt} ${pinentry_w32} \ + ${pinentry_qt} ${pinentry_qt5} ${pinentry_qt4} ${pinentry_tqt} ${pinentry_w32} \ ${pinentry_fltk} ${pinentry_efl} ${doc} Index: configure.ac =================================================================== --- configure.ac +++ configure.ac @@ -538,11 +538,27 @@ ) fi +dnl +dnl Check for KF6GuiAddons library +dnl +have_kf6guiaddons=no +if test "$have_w32_system" != "yes"; then + PKG_CHECK_MODULES( + KF6GUIADDONS, + [KF6GuiAddons >= 5.240], + [have_kf6guiaddons=yes], + [ + AC_MSG_WARN([pkg-config could not find the module KF6GuiAddons]) + have_kf6guiaddons=no + ] + ) +fi + dnl dnl Check for Qt5 pinentry program. dnl -AC_ARG_ENABLE(pinentry-qt, - AS_HELP_STRING([--enable-pinentry-qt],[build Qt5 pinentry]), +AC_ARG_ENABLE(pinentry-qt5, + AS_HELP_STRING([--enable-pinentry-qt5],[build Qt5 pinentry]), pinentry_qt5=$enableval, pinentry_qt5=maybe) @@ -569,21 +585,67 @@ AC_SUBST(PINENTRY_QT5_LDFLAGS) AC_SUBST(MOC5) -dnl If we have come so far, qt pinentry can be build. +dnl If we have come so far, qt5 pinentry can be build. if test "$pinentry_qt5" != "no"; then pinentry_qt5=yes fi AM_CONDITIONAL(BUILD_PINENTRY_QT5, test "$pinentry_qt5" = "yes") if test "$have_qt5_x11extras" = "yes"; then - AC_DEFINE(PINENTRY_QT_X11, 1, [pinentry-qt should use x11.]) + AC_DEFINE(PINENTRY_QT5_X11, 1, [pinentry-qt5 should use x11.]) fi if test "$have_kf5waylandclient" = "yes"; then PINENTRY_QT5_CFLAGS="$KF5WAYLANDCLIENT_CFLAGS $PINENTRY_QT5_CFLAGS -fpic" PINENTRY_QT5_LIBS="$KF5WAYLANDCLIENT_LIBS $PINENTRY_QT5_LIBS" - AC_DEFINE(PINENTRY_QT_WAYLAND, 1, [pinentry-qt should use KF5WaylandClient.]) + AC_DEFINE(PINENTRY_QT5_WAYLAND, 1, [pinentry-qt5 should use KF5WaylandClient.]) else if test "$have_w32_system" != "yes"; then - AC_MSG_WARN([pinentry-qt will be built without Caps Lock warning on Wayland]) + AC_MSG_WARN([pinentry-qt5 will be built without Caps Lock warning on Wayland]) + fi +fi + +dnl +dnl Check for Qt6 pinentry program. +dnl +AC_ARG_ENABLE(pinentry-qt, + AS_HELP_STRING([--enable-pinentry-qt],[build Qt6 pinentry]), + pinentry_qt6=$enableval, pinentry_qt6=maybe) + + +dnl +dnl Checks for qt libraries. Deal correctly with $pinentry_qt6 = maybe. +dnl Tries to find Qt6 +dnl +if test "$pinentry_qt6" != "no"; then + FIND_QT6 + if test "$have_qt6_libs" != "yes"; then + if test "$pinentry_qt6" = "yes"; then + AC_MSG_ERROR([[ + *** + *** Qt6 (Qt6Core, Qt6Gui, Qt6Widgets) is required. + ***]]) + else + pinentry_qt6=no + fi + fi +fi + +AC_SUBST(PINENTRY_QT6_CFLAGS) +AC_SUBST(PINENTRY_QT6_LIBS) +AC_SUBST(PINENTRY_QT6_LDFLAGS) +AC_SUBST(MOC6) + +dnl If we have come so far, qt pinentry can be build. +if test "$pinentry_qt6" != "no"; then + pinentry_qt6=yes +fi +AM_CONDITIONAL(BUILD_PINENTRY_QT6, test "$pinentry_qt6" = "yes") +if test "$have_kf6guiaddons" = "yes"; then + PINENTRY_QT6_CFLAGS="$KF6GUIADDONS_CFLAGS $PINENTRY_QT6_CFLAGS" + PINENTRY_QT6_LIBS="$KF6GUIADDONS_LIBS $PINENTRY_QT6_LIBS" + AC_DEFINE(PINENTRY_KGUIADDONS, 1, [pinentry-qt should use KF6GuiAddons.]) +else + if test "$have_w32_system" != "yes"; then + AC_MSG_WARN([pinentry-qt will be built without Caps Lock warning on Unix]) fi fi @@ -713,34 +775,38 @@ if test "$pinentry_gtk_2" = "yes"; then PINENTRY_DEFAULT=pinentry-gtk-2 else - if test "$pinentry_qt5" = "yes"; then + if test "$pinentry_qt6" = "yes"; then PINENTRY_DEFAULT=pinentry-qt else - if test "$pinentry_gnome_3" = "yes"; then - PINENTRY_DEFAULT=pinentry-gnome3 + if test "$pinentry_qt5" = "yes"; then + PINENTRY_DEFAULT=pinentry-qt5 else - if test "$pinentry_curses" = "yes"; then - PINENTRY_DEFAULT=pinentry-curses + if test "$pinentry_gnome_3" = "yes"; then + PINENTRY_DEFAULT=pinentry-gnome3 else - if test "$pinentry_tty" = "yes"; then - PINENTRY_DEFAULT=pinentry-tty + if test "$pinentry_curses" = "yes"; then + PINENTRY_DEFAULT=pinentry-curses else - if test "$pinentry_w32" = "yes"; then - PINENTRY_DEFAULT=pinentry-w32 + if test "$pinentry_tty" = "yes"; then + PINENTRY_DEFAULT=pinentry-tty else - if test "$pinentry_fltk" = "yes"; then - PINENTRY_DEFAULT=pinentry-fltk + if test "$pinentry_w32" = "yes"; then + PINENTRY_DEFAULT=pinentry-w32 else - if test "$pinentry_tqt" = "yes"; then - PINENTRY_DEFAULT=pinentry-tqt + if test "$pinentry_fltk" = "yes"; then + PINENTRY_DEFAULT=pinentry-fltk else - if test "$pinentry_efl" = "yes"; then - PINENTRY_DEFAULT=pinentry-efl + if test "$pinentry_tqt" = "yes"; then + PINENTRY_DEFAULT=pinentry-tqt else - if test "$pinentry_qt4" = "yes"; then - PINENTRY_DEFAULT=pinentry-qt4 + if test "$pinentry_efl" = "yes"; then + PINENTRY_DEFAULT=pinentry-efl else - AC_MSG_ERROR([[No pinentry enabled.]]) + if test "$pinentry_qt4" = "yes"; then + PINENTRY_DEFAULT=pinentry-qt4 + else + AC_MSG_ERROR([[No pinentry enabled.]]) + fi fi fi fi @@ -830,6 +896,8 @@ gnome3/Makefile qt/Makefile qt/icons/Makefile +qt5/Makefile +qt5/icons/Makefile qt4/Makefile tqt/Makefile w32/Makefile @@ -837,6 +905,7 @@ doc/Makefile Makefile qt/org.gnupg.pinentry-qt.desktop +qt5/org.gnupg.pinentry-qt5.desktop ]) AC_OUTPUT @@ -854,6 +923,7 @@ EFL Pinentry .....: $pinentry_efl GTK+-2 Pinentry ..: $pinentry_gtk_2 GNOME 3 Pinentry .: $pinentry_gnome_3 + Qt6 Pinentry .....: $pinentry_qt6 Qt5 Pinentry .....: $pinentry_qt5 Qt4 Pinentry .....: $pinentry_qt4 TQt Pinentry .....: $pinentry_tqt Index: doc/pinentry.texi =================================================================== --- doc/pinentry.texi +++ doc/pinentry.texi @@ -180,7 +180,7 @@ you are debugging software using the @pinentry{}; otherwise you may not be able to to access your X session anymore (unless you have other means to connect to the machine to kill the @pinentry{}). -Note that this feature only supported in flavors of Gtk+2 and Qt@tie{}4/5. +Note that this feature only supported in flavors of Gtk+2 and Qt@tie{}4/5/6. @item --parent-wid @var{n} @opindex parent-wid @@ -215,7 +215,7 @@ @chapter Front Ends There are several different flavors of @pinentry{}. Concretely, there -are Gtk+2, Qt@tie{}4/5, TQt, EFL, FLTK, Gnome@tie{}3, Emacs, curses and +are Gtk+2, Qt@tie{}4/5/6, TQt, EFL, FLTK, Gnome@tie{}3, Emacs, curses and tty variants. These different implementations provide higher levels of integration with a specific environment. For instance, the Gnome@tie{}3 @pinentry{} uses Gnome@tie{}3 widgets to display the Index: m4/qt6.m4 =================================================================== --- /dev/null +++ m4/qt6.m4 @@ -0,0 +1,103 @@ +dnl qt6.m4 +dnl Copyright (C) 2016 Intevation GmbH +dnl +dnl This file is part of pinentry and is provided under the same license as pinentry + +dnl Autoconf macro to find Qt6 +dnl +dnl sets PINENTRY_QT6_LIBS and PINENTRY_QT6_CFLAGS +dnl +dnl if QT6 was found have_qt6_libs is set to yes + +AC_DEFUN([FIND_QT6], +[ + have_qt6_libs="no"; + + PKG_CHECK_MODULES(PINENTRY_QT6, + Qt6Core >= 6.4.0 Qt6Gui >= 6.4.0 Qt6Widgets >= 6.4.0, + [have_qt6_libs="yes"], + [have_qt6_libs="no"]) + + PKG_CHECK_MODULES(PINENTRY_QT6TEST, + Qt6Test >= 6.4.0, + [have_qt6test_libs="yes"], + [have_qt6test_libs="no"]) + + if test "$have_qt6_libs" = "yes"; then + # Qt6 moved moc to libexec + qt6libexecdir=$($PKG_CONFIG --variable=libexecdir 'Qt6Core >= 6.4.0') + AC_PATH_TOOL(MOC6, moc, [], [$qt6libexecdir]) + if test -z "$MOC6"; then + AC_MSG_WARN([moc not found - Qt 6 binding will not be built.]) + have_qt6_libs="no"; + fi + AC_PATH_TOOL(RCC6, rcc, [], [$qt6libexecdir]) + if test -z "$RCC6"; then + AC_MSG_WARN([rcc not found - Qt 6 binding will not be built.]) + have_qt6_libs="no"; + fi + fi + + if test "$have_qt6_libs" = "yes"; then + if test "$have_w32_system" != yes; then + mkspecsdir=$($PKG_CONFIG --variable mkspecsdir Qt6Platform) + if test -z "$mkspecsdir"; then + AC_MSG_WARN([Failed to determine Qt's mkspecs directory. Cannot check its build configuration.]) + fi + fi + + # check if we need -fPIC + if test -z "$use_reduce_relocations" && test -n "$mkspecsdir"; then + AC_MSG_CHECKING([whether Qt was built with -fPIC]) + if grep -q "QT_CONFIG .* reduce_relocations" $mkspecsdir/qconfig.pri; then + use_reduce_relocations="yes" + else + use_reduce_relocations="no" + fi + AC_MSG_RESULT([$use_reduce_relocations]) + fi + if test "$use_reduce_relocations" = yes; then + PINENTRY_QT6_CFLAGS="$PINENTRY_QT6_CFLAGS -fPIC" + fi + + # check if we need -mno-direct-extern-access + if test "$have_no_direct_extern_access" = yes; then + if test -z "$use_no_direct_extern_access" && test -n "$mkspecsdir"; then + AC_MSG_CHECKING([whether Qt was built with -mno-direct-extern-access]) + if grep -q "QT_CONFIG .* no_direct_extern_access" $mkspecsdir/qconfig.pri; then + use_no_direct_extern_access="yes" + else + use_no_direct_extern_access="no" + fi + AC_MSG_RESULT([$use_no_direct_extern_access]) + fi + if test "$use_no_direct_extern_access" = yes; then + PINENTRY_QT6_CFLAGS="$PINENTRY_QT6_CFLAGS -mno-direct-extern-access" + fi + fi + + dnl Check that a binary can actually be build with this qt. + dnl pkg-config may be set up in a way that it looks also for libraries + dnl of the build system and not only for the host system. In that case + dnl we check here that we can actually compile / link a qt application + dnl for host. + OLDCPPFLAGS=$CPPFLAGS + OLDLIBS=$LIBS + + CPPFLAGS=$PINENTRY_QT6_CFLAGS + LIBS=$PINENTRY_QT6_LIBS + AC_LANG_PUSH(C++) + AC_MSG_CHECKING([whether a simple Qt program can be built]) + AC_LINK_IFELSE([AC_LANG_SOURCE([ + #include + int main (int argc, char **argv) { + QCoreApplication app(argc, argv); + app.exec(); + }])], [have_qt6_libs='yes'], [have_qt6_libs='no']) + AC_MSG_RESULT([$have_qt6_libs]) + AC_LANG_POP() + + CPPFLAGS=$OLDCPPFLAGS + LIBS=$OLDLIBS + fi +]) Index: qt/Makefile.am =================================================================== --- qt/Makefile.am +++ qt/Makefile.am @@ -39,13 +39,13 @@ AM_CPPFLAGS = $(COMMON_CFLAGS) \ -I$(top_srcdir) -I$(top_srcdir)/secmem \ $(ncurses_include) -I$(top_srcdir)/pinentry -AM_CXXFLAGS = $(PINENTRY_QT5_CFLAGS) +AM_CXXFLAGS = $(PINENTRY_QT6_CFLAGS) pinentry_qt_LDADD = \ ../pinentry/libpinentry.a $(top_builddir)/secmem/libsecmem.a \ - $(COMMON_LIBS) $(PINENTRY_QT5_LIBS) $(libcurses) -pinentry_qt_LDFLAGS = $(PINENTRY_QT5_LDFLAGS) + $(COMMON_LIBS) $(PINENTRY_QT6_LIBS) $(libcurses) +pinentry_qt_LDFLAGS = $(PINENTRY_QT6_LDFLAGS) -if BUILD_PINENTRY_QT5 +if BUILD_PINENTRY_QT6 BUILT_SOURCES = \ pinentryconfirm.moc pinentrydialog.moc pinlineedit.moc capslock.moc \ focusframe.moc \ @@ -73,10 +73,10 @@ nodist_pinentry_qt_SOURCES = $(BUILT_SOURCES) .h.moc: - $(MOC5) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ + $(MOC6) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ pinentryrc.cpp: pinentryrc.qrc - $(RCC5) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ + $(RCC6) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ desktopdir = $(datadir)/applications desktop_DATA = org.gnupg.pinentry-qt.desktop Index: qt/capslock.cpp =================================================================== --- qt/capslock.cpp +++ qt/capslock.cpp @@ -32,10 +32,8 @@ CapsLockWatcher::Private::Private(CapsLockWatcher *q) : q{q} { -#ifdef PINENTRY_QT_WAYLAND - if (qApp->platformName() == QLatin1String("wayland")) { - watchWayland(); - } +#ifdef PINENTRY_KGUIADDONS + watch(); #endif } @@ -43,9 +41,9 @@ : QObject{parent} , d{new Private{this}} { - if (qApp->platformName() == QLatin1String("wayland")) { -#ifndef PINENTRY_QT_WAYLAND - qWarning() << "CapsLockWatcher was compiled without support for Wayland"; + if (qApp->platformName() == QLatin1String("wayland") || qApp->platformName() == QLatin1String("xcb")) { +#ifndef PINENTRY_KGUIADDONS + qWarning() << "CapsLockWatcher was compiled without support for unix"; #endif } } Index: qt/capslock_p.h =================================================================== --- qt/capslock_p.h +++ qt/capslock_p.h @@ -23,39 +23,23 @@ #include "capslock.h" -#ifdef PINENTRY_QT_WAYLAND -namespace KWayland -{ -namespace Client -{ -class Registry; -class Seat; -} -} +#ifdef PINENTRY_KGUIADDONS +#include #endif 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); +#ifdef PINENTRY_KGUIADDONS + void watch(); + KModifierKeyInfo *keyInfo = nullptr; #endif +private: private: CapsLockWatcher *const q; - -#ifdef PINENTRY_QT_WAYLAND - KWayland::Client::Registry *registry = nullptr; - KWayland::Client::Seat *seat = nullptr; -#endif }; #endif // __PINENTRY_QT_CAPSLOCK_P_H__ Index: qt/capslock_unix.cpp =================================================================== --- qt/capslock_unix.cpp +++ qt/capslock_unix.cpp @@ -1,5 +1,5 @@ /* capslock_unix.cpp - Helper to check whether Caps Lock is on - * Copyright (C) 2021 g10 Code GmbH + * Copyright (C) 2021, 2024 g10 Code GmbH * * Software engineering by Ingo Klöcker * @@ -25,112 +25,36 @@ #include "capslock.h" #include "capslock_p.h" -#ifdef PINENTRY_QT_WAYLAND -# include -# include -# include -# include -#endif - -#include - -#ifdef PINENTRY_QT_X11 -# include -# include -# undef Status -#endif - #include - -#ifdef PINENTRY_QT_WAYLAND -using namespace KWayland::Client; -#endif - -#ifdef PINENTRY_QT_WAYLAND -static bool watchingWayland = false; -#endif +#include 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 + +#ifdef PINENTRY_KGUIADDONS + if (qApp->platformName() != QLatin1String("wayland") && qApp->platformName() != QLatin1String("xcb") && reportUnsupportedPlatform) { +#else if (reportUnsupportedPlatform) { +#endif qWarning() << "Checking for Caps Lock not possible on unsupported platform:" << qApp->platformName(); } reportUnsupportedPlatform = false; +#ifdef PINENTRY_KGUIADDONS + static KModifierKeyInfo keyInfo; + return keyInfo.isKeyLocked(Qt::Key_CapsLock) ? LockState::On : LockState::Off; +#endif 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) +#ifdef PINENTRY_KGUIADDONS +void CapsLockWatcher::Private::watch() { - 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); + keyInfo = new KModifierKeyInfo(); + connect(keyInfo, &KModifierKeyInfo::keyLocked, q, [=](Qt::Key key, bool locked){ + if (key == Qt::Key_CapsLock) { + Q_EMIT q->stateChanged(locked); + } + }); } #endif Index: qt/main.cpp =================================================================== --- qt/main.cpp +++ qt/main.cpp @@ -356,7 +356,6 @@ main(int argc, char *argv[]) { pinentry_init("pinentry-qt"); - QApplication *app = NULL; int new_argc = 0; Index: qt/pinentryconfirm.cpp =================================================================== --- qt/pinentryconfirm.cpp +++ qt/pinentryconfirm.cpp @@ -116,7 +116,7 @@ _timed_out = true; if (b) { - b->animateClick(0); + b->animateClick(); } } Index: qt5/Makefile.am =================================================================== --- /dev/null +++ qt5/Makefile.am @@ -0,0 +1,82 @@ +# Makefile.am +# Copyright (C) 2002 g10 Code GmbH, Klarälvdalens Datakonsult AB +# Copyright (C) 2008, 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 . +# SPDX-License-Identifier: GPL-2.0+ + +## Process this file with automake to produce Makefile.in + +bin_PROGRAMS = pinentry-qt5 + +EXTRA_DIST = pinentryrc.qrc + +SUBDIRS = icons + + +if FALLBACK_CURSES +ncurses_include = $(NCURSES_INCLUDE) +libcurses = ../pinentry/libpinentry-curses.a $(LIBCURSES) $(LIBICONV) +else +ncurses_include = +libcurses = +endif + + +AM_CPPFLAGS = $(COMMON_CFLAGS) \ + -I$(top_srcdir) -I$(top_srcdir)/secmem \ + $(ncurses_include) -I$(top_srcdir)/pinentry +AM_CXXFLAGS = $(PINENTRY_QT5_CFLAGS) +pinentry_qt5_LDADD = \ + ../pinentry/libpinentry.a $(top_builddir)/secmem/libsecmem.a \ + $(COMMON_LIBS) $(PINENTRY_QT5_LIBS) $(libcurses) +pinentry_qt5_LDFLAGS = $(PINENTRY_QT5_LDFLAGS) + +if BUILD_PINENTRY_QT5 +BUILT_SOURCES = \ + pinentryconfirm.moc pinentrydialog.moc pinlineedit.moc capslock.moc \ + focusframe.moc \ + keyboardfocusindication.moc \ + pinentryrc.cpp +endif + +CLEANFILES = $(BUILT_SOURCES) + +if HAVE_W32_SYSTEM +pinentry_qt5_platform_SOURCES = capslock_win.cpp +else +pinentry_qt5_platform_SOURCES = capslock_unix.cpp +endif + +pinentry_qt5_SOURCES = pinentrydialog.h pinentrydialog.cpp \ + main.cpp pinentryconfirm.cpp pinentryconfirm.h \ + pinlineedit.h pinlineedit.cpp capslock.cpp capslock.h capslock_p.h \ + pinentry_debug.cpp pinentry_debug.h util.h accessibility.cpp \ + accessibility.h qti18n.cpp pinentryrc.qrc \ + focusframe.h focusframe.cpp \ + keyboardfocusindication.h keyboardfocusindication.cpp \ + $(pinentry_qt5_platform_SOURCES) + +nodist_pinentry_qt5_SOURCES = $(BUILT_SOURCES) + +.h.moc: + $(MOC5) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ + +pinentryrc.cpp: pinentryrc.qrc + $(RCC5) `test -f '$<' || echo '$(srcdir)/'`$< -o $@ + +desktopdir = $(datadir)/applications +desktop_DATA = org.gnupg.pinentry-qt5.desktop Index: qt5/accessibility.h =================================================================== --- /dev/null +++ qt5/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 + * + * 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 . + * 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__ Index: qt5/accessibility.cpp =================================================================== --- /dev/null +++ qt5/accessibility.cpp @@ -0,0 +1,47 @@ +/* accessibility.cpp - Helpers for making pinentry accessible + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "accessibility.h" + +#include +#include + +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 Index: qt5/capslock.h =================================================================== --- /dev/null +++ qt5/capslock.h @@ -0,0 +1,52 @@ +/* capslock.h - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_CAPSLOCK_H__ +#define __PINENTRY_QT_CAPSLOCK_H__ + +#include + +#include + +enum class LockState +{ + Unknown = -1, + Off, + On +}; + +LockState capsLockState(); + +class CapsLockWatcher : public QObject +{ + Q_OBJECT + +public: + explicit CapsLockWatcher(QObject *parent = nullptr); + +Q_SIGNALS: + void stateChanged(bool locked); + +private: + class Private; + std::unique_ptr d; +}; + +#endif // __PINENTRY_QT_CAPSLOCK_H__ Index: qt5/capslock.cpp =================================================================== --- /dev/null +++ qt5/capslock.cpp @@ -0,0 +1,53 @@ +/* capslock.cpp - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "capslock.h" +#include "capslock_p.h" + +#include + +#include + +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 + } +} + +#include "capslock.moc" Index: qt5/capslock_p.h =================================================================== --- /dev/null +++ qt5/capslock_p.h @@ -0,0 +1,61 @@ +/* capslock_p.h - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_CAPSLOCK_P_H__ +#define __PINENTRY_QT_CAPSLOCK_P_H__ + +#include "capslock.h" + +#ifdef PINENTRY_QT_WAYLAND +namespace KWayland +{ +namespace Client +{ +class Registry; +class Seat; +} +} +#endif + +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_P_H__ Index: qt5/capslock_unix.cpp =================================================================== --- /dev/null +++ qt5/capslock_unix.cpp @@ -0,0 +1,136 @@ +/* capslock_unix.cpp - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "capslock.h" +#include "capslock_p.h" + +#ifdef PINENTRY_QT_WAYLAND +# include +# include +# include +# include +#endif + +#include + +#ifdef PINENTRY_QT_X11 +# include +# include +# undef Status +#endif + +#include + +#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 Index: qt5/capslock_win.cpp =================================================================== --- /dev/null +++ qt5/capslock_win.cpp @@ -0,0 +1,28 @@ +/* capslock_win.cpp - Helper to check whether Caps Lock is on + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "capslock.h" + +#include + +LockState capsLockState() +{ + return (GetKeyState(VK_CAPITAL) & 1) ? LockState::On : LockState::Off; +} Index: qt5/focusframe.h =================================================================== --- /dev/null +++ qt5/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 + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __FOCUSFRAME_H__ +#define __FOCUSFRAME_H__ + +#include + +class FocusFrame : public QFocusFrame +{ + Q_OBJECT +public: + using QFocusFrame::QFocusFrame; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +#endif // __FOCUSFRAME_H__ Index: qt5/focusframe.cpp =================================================================== --- /dev/null +++ qt5/focusframe.cpp @@ -0,0 +1,74 @@ +/* focusframe.cpp - A focus indicator for labels. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "focusframe.h" + +#if QT_CONFIG(graphicseffect) +#include +#endif +#include +#include + +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); +} + +#include "focusframe.moc" Index: qt5/icons/Makefile.am =================================================================== --- /dev/null +++ qt5/icons/Makefile.am @@ -0,0 +1,26 @@ +# Makefile.am +# Copyright (C) 2022 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 . +# SPDX-License-Identifier: GPL-2.0+ + +## Process this file with automake to produce Makefile.in + +EXTRA_DIST = data-error.svg \ + document-encrypt.png \ + hint.svg \ + password-generate.svg \ + visibility.svg Index: qt5/icons/data-error.svg =================================================================== --- /dev/null +++ qt5/icons/data-error.svg @@ -0,0 +1,9 @@ + + + + + Index: qt5/icons/hint.svg =================================================================== --- /dev/null +++ qt5/icons/hint.svg @@ -0,0 +1,13 @@ + + + + + + Index: qt5/icons/password-generate.svg =================================================================== --- /dev/null +++ qt5/icons/password-generate.svg @@ -0,0 +1,13 @@ + + + + + + Index: qt5/icons/visibility.svg =================================================================== --- /dev/null +++ qt5/icons/visibility.svg @@ -0,0 +1,21 @@ + + + + + + + + + + Index: qt5/keyboardfocusindication.h =================================================================== --- /dev/null +++ qt5/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 + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __KEYBOARDFOCUSINDICATION_H__ +#define __KEYBOARDFOCUSINDICATION_H__ + +#include +#include +#include + +class FocusFrame; + +class KeyboardFocusIndication : public QObject +{ + Q_OBJECT +public: + KeyboardFocusIndication(QObject *parent); + +private: + void updateFocusFrame(QWidget *, QWidget *); + + QPointer focusFrame; +}; + +#endif // __KEYBOARDFOCUSINDICATION_H__ Index: qt5/keyboardfocusindication.cpp =================================================================== --- /dev/null +++ qt5/keyboardfocusindication.cpp @@ -0,0 +1,45 @@ +/* keyboardfocusindication.cpp - Helper for extended keyboard focus indication. + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "keyboardfocusindication.h" + +#include "focusframe.h" + +#include + +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); + } +} + +#include "keyboardfocusindication.moc" Index: qt5/main.cpp =================================================================== --- /dev/null +++ qt5/main.cpp @@ -0,0 +1,435 @@ +/* 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 . + * Modified by Marcus Brinkmann . + * Modified by Marc Mutz + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "accessibility.h" +#include "keyboardfocusindication.h" +#include "pinentryconfirm.h" +#include "pinentrydialog.h" +#include "pinentry.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#if QT_VERSION >= 0x050000 +#include +#endif + +#include +#include + +#include +#include + +#ifdef FALLBACK_CURSES +#include +#endif + +#if QT_VERSION >= 0x050000 && defined(QT_STATIC) + #include + #ifdef Q_OS_WIN + #include + #include + 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 +#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() {} +}; +} + +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 str{pinentry_get_title(pe)}; + const QString title = + str ? from_utf8(str.get()) : + /* else */ QLatin1String("pinentry-qt5") ; + + 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(); + + const QString savePassphraseText = + pe->default_pwmngr ? escape_accel(from_utf8(pe->default_pwmngr)) : + QStringLiteral("Save passphrase in password manager"); + + if (want_pass) { + PinEntryDialog pinentry(pe, nullptr, 0, true, + repeatString, visibilityTT, hideTT); + setup_foreground_window(&pinentry, pe->parent_wid); + 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) + }); + pinentry.setSavePassphraseCBText(savePassphraseText); + + 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 +main(int argc, char *argv[]) +{ + pinentry_init("pinentry-qt5"); + + 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-qt5: 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(":/icons/document-encrypt.png"))); + app->setDesktopFileName(QStringLiteral("org.gnupg.pinentry-qt5")); + (void) new KeyboardFocusIndication{app}; + } + + pinentry_parse_opts(argc, argv); + + int rc = pinentry_loop(); + delete app; + return rc ? EXIT_FAILURE : EXIT_SUCCESS ; +} Index: qt5/org.gnupg.pinentry-qt5.desktop.in =================================================================== --- /dev/null +++ qt5/org.gnupg.pinentry-qt5.desktop.in @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=Pinentry +Type=Application +Categories=Qt;Utility; +Exec=@prefix@/bin/pinentry-qt5 +NoDisplay=true +Icon=document-encrypt +GenericName=Utility for entering GnuPG secrets +X-KDE-Wayland-Interfaces=org_kde_kwin_keystate Index: qt5/pinentry_debug.h =================================================================== --- /dev/null +++ qt5/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 + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_DEBUG_H__ +#define __PINENTRY_QT_DEBUG_H__ + +#include + +Q_DECLARE_LOGGING_CATEGORY(PINENTRY_LOG) + +#endif // __PINENTRY_QT_DEBUG_H__ Index: qt5/pinentry_debug.cpp =================================================================== --- /dev/null +++ qt5/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 + * + * 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 . + * 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 Index: qt5/pinentryconfirm.h =================================================================== --- /dev/null +++ qt5/pinentryconfirm.h @@ -0,0 +1,63 @@ +/* pinentryconfirm.h - A QMessageBox with a timeout + * + * Copyright (C) 2011 Ben Kibbey + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef PINENTRYCONFIRM_H +#define PINENTRYCONFIRM_H + +#include +#include +#include + +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 Index: qt5/pinentryconfirm.cpp =================================================================== --- /dev/null +++ qt5/pinentryconfirm.cpp @@ -0,0 +1,134 @@ +/* pinentryconfirm.cpp - A QMessageBox with a timeout + * + * Copyright (C) 2011 Ben Kibbey + * Copyright (C) 2022 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "pinentryconfirm.h" + +#include "accessibility.h" +#include "pinentrydialog.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +QLabel *messageBoxLabel(QMessageBox *messageBox) +{ + return messageBox->findChild(QStringLiteral("qt_msgbox_label")); +} +} + +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(_timer.intervalAsDuration()); +} + +bool PinentryConfirm::timedOut() const +{ + return _timed_out; +} + +void PinentryConfirm::showEvent(QShowEvent *event) +{ + static bool resized; + if (!resized) { + QGridLayout* lay = dynamic_cast (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(0); + } +} + +#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 + +#include "pinentryconfirm.moc" Index: qt5/pinentrydialog.h =================================================================== --- /dev/null +++ qt5/pinentrydialog.h @@ -0,0 +1,182 @@ +/* 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 . + * Modified by Andre Heinecke + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRYDIALOG_H__ +#define __PINENTRYDIALOG_H__ + +#include +#include +#include +#include + +#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(pinentry_t pe, QWidget *parent = 0, const char *name = 0, + bool modal = 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 setSavePassphraseCBText(const QString &text); + + 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(); + void togglePasswordCaching(bool enabled); + +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; + QCheckBox *mSavePassphraseCB = nullptr; +}; + +#endif // __PINENTRYDIALOG_H__ Index: qt5/pinentrydialog.cpp =================================================================== --- /dev/null +++ qt5/pinentrydialog.cpp @@ -0,0 +1,780 @@ +/* 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 . + * Modified by Andre Heinecke + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "pinentrydialog.h" + +#include "accessibility.h" +#include "capslock.h" +#include "pinlineedit.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef Q_OS_WIN +#include +#if QT_VERSION >= 0x050700 +#include +#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(pinentry_t pe, QWidget *parent, const char *name, + bool modal, + const QString &repeatString, + const QString &visibilityTT, + const QString &hideTT) + : QDialog{parent} + , _have_quality_bar{!!pe->quality_bar} + , _pinentry_info{pe} + , mVisibilityTT{visibilityTT} + , mHideTT{hideTT} +{ + Q_UNUSED(name) + + if (modal) { + setWindowModality(Qt::ApplicationModal); + } + + 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(":/icons/password-generate"))); + mGenerateButton->setVisible(false); + l->addWidget(mGenerateButton); + } + grid->addLayout(l, row, 2); + } + + /* Set up the show password action */ + const QIcon visibilityIcon = QIcon(QLatin1String(":/icons/visibility.svg")); + const QIcon hideIcon = QIcon(QLatin1String(":/icons/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 (_have_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); + } + + ++row; + mSavePassphraseCB = new QCheckBox{this}; + mSavePassphraseCB->setVisible(false); + mSavePassphraseCB->setCheckState(!!_pinentry_info->may_cache_password + ? Qt::Checked + : Qt::Unchecked); +#ifdef HAVE_LIBSECRET + if (_pinentry_info->allow_external_password_cache && _pinentry_info->keyinfo) { + mSavePassphraseCB->setVisible(true); + } +#endif + grid->addWidget(mSavePassphraseCB, row, 1, 1, 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 (_pinentry_info->timeout > 0) { + _timer = new QTimer(this); + connect(_timer, &QTimer::timeout, this, &PinEntryDialog::slotTimeout); + _timer->start(_pinentry_info->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); + } + connect(mSavePassphraseCB, &QCheckBox::toggled, + this, &PinEntryDialog::togglePasswordCaching); + + 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(); + +#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(":/icons/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("") + options.hint.toHtmlEscaped() + QLatin1String("")); + 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("") + + options.longHint.toHtmlEscaped().replace(QLatin1String("\n\n"), QLatin1String("
")) + + QLatin1String("")); + 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::togglePasswordCaching(bool enabled) +{ + _pinentry_info->may_cache_password = enabled; +} + +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::setSavePassphraseCBText(const QString &text) +{ + mSavePassphraseCB->setText(text); +} + +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 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(":/icons/hint.svg"))); + mVisiActionEdit->setToolTip(mHideTT); + } + _edit->setEchoMode(QLineEdit::Normal); + if (mRepeat) { + mRepeat->setEchoMode(QLineEdit::Normal); + } + } else { + if (mVisiActionEdit) { + mVisiActionEdit->setIcon(QIcon(QLatin1String(":/icons/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 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; +} + +#include "pinentrydialog.moc" Index: qt5/pinentryrc.qrc =================================================================== --- /dev/null +++ qt5/pinentryrc.qrc @@ -0,0 +1,10 @@ + + + + icons/data-error.svg + icons/document-encrypt.png + icons/hint.svg + icons/password-generate.svg + icons/visibility.svg + + Index: qt5/pinlineedit.h =================================================================== --- /dev/null +++ qt5/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 + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef _PINLINEEDIT_H_ +#define _PINLINEEDIT_H_ + +#include + +#include + +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 d; +}; + +#endif // _PINLINEEDIT_H_ Index: qt5/pinlineedit.cpp =================================================================== --- /dev/null +++ qt5/pinlineedit.cpp @@ -0,0 +1,232 @@ +/* pinlineedit.cpp - Modified QLineEdit widget. + * Copyright (C) 2018 Damien Goutte-Gattat + * Copyright (C) 2021 g10 Code GmbH + * + * Software engineering by Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "pinlineedit.h" + +#include +#include +#include + +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; +}; +} + +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); +} + +#include "pinlineedit.moc" Index: qt5/qti18n.cpp =================================================================== --- /dev/null +++ qt5/qti18n.cpp @@ -0,0 +1,93 @@ +/* qti18n.cpp - Load qt translations for pinentry. + * Copyright 2021 g10 Code GmbH + * SPDX-FileCopyrightText: 2015 Lukáš Tinkl + * SPDX-FileCopyrightText: 2021 Ingo Klöcker + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include +#include +#include +#include +#include + +#include + +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) Index: qt5/util.h =================================================================== --- /dev/null +++ qt5/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 + * + * 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 . + * SPDX-License-Identifier: GPL-2.0+ + */ + +#ifndef __PINENTRY_QT_UTIL_H__ +#define __PINENTRY_QT_UTIL_H__ + +#include + +#include + +namespace _detail +{ +struct FreeDeleter { + void operator()(void *ptr) const { + free(ptr); + } +}; +} + +template +using unique_malloced_ptr = std::unique_ptr; + +#endif // __PINENTRY_QT_UTIL_H__