Automatically detect USB device changes

This commit is contained in:
Janek Bevendorff 2023-12-10 19:48:43 +01:00 committed by Jonathan White
parent 79ca00604a
commit 6a273363c4
41 changed files with 1503 additions and 823 deletions

View File

@ -570,6 +570,12 @@ include_directories(SYSTEM ${ZLIB_INCLUDE_DIR})
if(WITH_XC_YUBIKEY) if(WITH_XC_YUBIKEY)
find_package(PCSC REQUIRED) find_package(PCSC REQUIRED)
include_directories(SYSTEM ${PCSC_INCLUDE_DIRS}) include_directories(SYSTEM ${PCSC_INCLUDE_DIRS})
if(UNIX AND NOT APPLE)
find_library(LIBUSB_LIBRARIES NAMES usb-1.0 REQUIRED)
find_path(LIBUSB_INCLUDE_DIR NAMES libusb.h PATH_SUFFIXES "libusb-1.0" "libusb" REQUIRED)
include_directories(SYSTEM ${LIBUSB_INCLUDE_DIR})
endif()
endif() endif()
if(UNIX) if(UNIX)

View File

@ -221,6 +221,7 @@ Files: share/icons/application/scalable/actions/application-exit.svg
share/icons/application/scalable/actions/username-copy.svg share/icons/application/scalable/actions/username-copy.svg
share/icons/application/scalable/actions/view-history.svg share/icons/application/scalable/actions/view-history.svg
share/icons/application/scalable/actions/web.svg share/icons/application/scalable/actions/web.svg
share/icons/application/scalable/actions/yubikey-refresh.svg
share/icons/application/scalable/apps/internet-web-browser.svg share/icons/application/scalable/apps/internet-web-browser.svg
share/icons/application/scalable/apps/keepassxc.svg share/icons/application/scalable/apps/keepassxc.svg
share/icons/application/scalable/apps/keepassxc-dark.svg share/icons/application/scalable/apps/keepassxc-dark.svg

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(0.722673,-0.722673,0.722673,0.722673,-9.84661,10.66)">
<path d="M6.086,22.736C3.148,21.904 1,19.206 1,16C1,14.07 1.78,12.32 3.05,11.05L9.77,4.33L11.288,5.868L15.078,2.078C15.258,1.898 15.508,1.788 15.788,1.788C16.068,1.788 16.318,1.898 16.498,2.078L21.919,7.502L21.919,7.512C22.059,7.682 22.139,7.902 22.139,8.142C22.139,8.442 22.009,8.712 21.799,8.902L18.069,12.642L19.67,14.23L18.255,15.645L9.77,7.16L4.46,12.46C3.56,13.37 3,14.62 3,16C3,18.186 4.405,20.046 6.361,20.725C6.182,21.382 6.09,22.059 6.086,22.736ZM7.133,18.873C5.897,18.502 5,17.358 5,16C5,14.34 6.34,13 8,13C9.505,13 10.747,14.102 10.966,15.545C10.021,15.925 9.136,16.499 8.371,17.264C7.879,17.756 7.466,18.298 7.133,18.873ZM20.35,8.046L15.859,3.553L12.431,7.001L16.901,11.484L20.35,8.046ZM8,15C7.45,15 7,15.45 7,16C7,16.55 7.45,17 8,17C8.55,17 9,16.55 9,16C9,15.45 8.55,15 8,15Z"/>
</g>
<g transform="matrix(0.832215,6.28971e-17,-6.28971e-17,0.832215,6.96368,6.76821)">
<path d="M17.65,6.35C16.2,4.9 14.21,4 12,4C7.611,4 4,7.611 4,12C4,16.389 7.611,20 12,20C15.73,20 18.84,17.45 19.73,14L17.65,14C16.83,16.33 14.61,18 12,18C8.708,18 6,15.292 6,12C6,8.708 8.708,6 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11L20,11L20,4L17.65,6.35Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -87,6 +87,7 @@
<file>application/scalable/actions/username-copy.svg</file> <file>application/scalable/actions/username-copy.svg</file>
<file>application/scalable/actions/view-history.svg</file> <file>application/scalable/actions/view-history.svg</file>
<file>application/scalable/actions/web.svg</file> <file>application/scalable/actions/web.svg</file>
<file>application/scalable/actions/yubikey-refresh.svg</file>
<file>application/scalable/apps/freedesktop.svg</file> <file>application/scalable/apps/freedesktop.svg</file>
<file>application/scalable/apps/internet-web-browser.svg</file> <file>application/scalable/apps/internet-web-browser.svg</file>
<file>application/scalable/apps/keepassxc.svg</file> <file>application/scalable/apps/keepassxc.svg</file>

View File

@ -1500,39 +1500,10 @@ Backup database located at %2</source>
<source>Password field</source> <source>Password field</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Enter Additional Credentials (if any):</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Key File:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;p&gt;In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database&apos;s security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;br&gt;If you do not have a key file, leave this field empty.&lt;/p&gt;&lt;p&gt;Click for more information&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Key file help</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Hardware key slot selection</source> <source>Hardware key slot selection</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Hardware Key:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;p&gt;You can use a hardware security key such as a &lt;strong&gt;YubiKey&lt;/strong&gt; or &lt;strong&gt;OnlyKey&lt;/strong&gt; with slots configured for HMAC-SHA1.&lt;/p&gt;
&lt;p&gt;Click for more information&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key help</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Key file to unlock the database</source> <source>Key file to unlock the database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1545,14 +1516,6 @@ Backup database located at %2</source>
<source>Browse</source> <source>Browse</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Refresh hardware tokens</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Unlock Database</source> <source>Unlock Database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1638,23 +1601,6 @@ To prevent this error from appearing, you must go to &quot;Database Settings / S
<source>Cannot use database file as key file</source> <source>Cannot use database file as key file</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>You cannot use your database file as a key file.
If you do not have a key file, please leave the field empty.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Detecting hardware keys</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No hardware keys detected</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select hardware key</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>authenticate to access the database</source> <source>authenticate to access the database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1663,6 +1609,54 @@ If you do not have a key file, please leave the field empty.</source>
<source>Failed to authenticate with Quick Unlock: %1</source> <source>Failed to authenticate with Quick Unlock: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Select Key File:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;p&gt;In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database&apos;s security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Click to add a key file.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;a href=&quot;#&quot; style=&quot;text-decoration: underline&quot;&gt;I have a key file&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use hardware key [Serial: %1]</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use hardware key</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Your database file is NOT a key file!
If you don&apos;t have a key file or don&apos;t know what that is, you don&apos;t have to select one.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KeePassXC database file selected</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The file you selected looks like a database file.
A database file is NOT a key file!
Are you sure you want to continue with this file?.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No hardware keys found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh Hardware Keys</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DatabaseSettingWidgetMetaData</name> <name>DatabaseSettingWidgetMetaData</name>
@ -9633,10 +9627,6 @@ Example: JBSWY3DPEHPK3PXP</source>
</context> </context>
<context> <context>
<name>YubiKey</name> <name>YubiKey</name>
<message>
<source>%1 No interface, slot %2</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>General: </source> <source>General: </source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -9648,14 +9638,6 @@ Example: JBSWY3DPEHPK3PXP</source>
</context> </context>
<context> <context>
<name>YubiKeyEditWidget</name> <name>YubiKeyEditWidget</name>
<message>
<source>Refresh hardware tokens</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Hardware key slot selection</source> <source>Hardware key slot selection</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -9700,28 +9682,17 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>&lt;p&gt;If you own a &lt;a href=&quot;https://www.yubico.com/&quot;&gt;YubiKey&lt;/a&gt; or &lt;a href=&quot;https://onlykey.io&quot;&gt;OnlyKey&lt;/a&gt;, you can use it for additional security.&lt;/p&gt;&lt;p&gt;The key requires one of its slots to be programmed as &lt;a href=&quot;https://docs.yubico.com/yesdk/users-manual/application-otp/challenge-response.html&quot;&gt;HMAC-SHA1 Challenge-Response&lt;/a&gt;.&lt;/p&gt;</source> <source>&lt;p&gt;If you own a &lt;a href=&quot;https://www.yubico.com/&quot;&gt;YubiKey&lt;/a&gt; or &lt;a href=&quot;https://onlykey.io&quot;&gt;OnlyKey&lt;/a&gt;, you can use it for additional security.&lt;/p&gt;&lt;p&gt;The key requires one of its slots to be programmed as &lt;a href=&quot;https://docs.yubico.com/yesdk/users-manual/application-otp/challenge-response.html&quot;&gt;HMAC-SHA1 Challenge-Response&lt;/a&gt;.&lt;/p&gt;</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context>
<context>
<name>YubiKeyInterface</name>
<message> <message>
<source>%1 Invalid slot specified - %2</source> <source>Refresh hardware keys</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context> <context>
<name>YubiKeyInterfacePCSC</name> <name>YubiKeyInterfacePCSC</name>
<message>
<source>(PCSC) %1 [%2] Challenge-Response - Slot %3</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>The YubiKey PCSC interface has not been initialized.</source> <source>The YubiKey PCSC interface has not been initialized.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Hardware key is currently in use.</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Could not find or access hardware key with serial number %1. Please present it to continue. </source> <source>Could not find or access hardware key with serial number %1. Please present it to continue. </source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -9738,6 +9709,21 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Failed to complete a challenge-response, the PCSC error code was: %1</source> <source>Failed to complete a challenge-response, the PCSC error code was: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>(NFC) %1 [%2] - Slot %3, %4</source>
<comment>YubiKey display fields</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Press</source>
<comment>USB Challenge-Response Key interaction request</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Passive</source>
<comment>USB Challenge-Response Key no interaction required</comment>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>YubiKeyInterfaceUSB</name> <name>YubiKeyInterfaceUSB</name>
@ -9745,14 +9731,6 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Unknown</source> <source>Unknown</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>(USB) %1 [%2] Configured Slot - %3</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>(USB) %1 [%2] Challenge-Response - Slot %3 - %4</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Press</source> <source>Press</source>
<comment>USB Challenge-Response Key interaction request</comment> <comment>USB Challenge-Response Key interaction request</comment>
@ -9767,10 +9745,6 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>The YubiKey USB interface has not been initialized.</source> <source>The YubiKey USB interface has not been initialized.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Hardware key is currently in use.</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Could not find hardware key with serial number %1. Please plug it in to continue.</source> <source>Could not find hardware key with serial number %1. Please plug it in to continue.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -9787,5 +9761,15 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Failed to complete a challenge-response, the specific error was: %1</source> <source>Failed to complete a challenge-response, the specific error was: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>%1 [%2] - Slot %3</source>
<comment>YubiKey NEO display fields</comment>
<translation type="unfinished"></translation>
</message>
<message>
<source>%1 [%2] - Slot %3, %4</source>
<comment>YubiKey display fields</comment>
<translation type="unfinished"></translation>
</message>
</context> </context>
</TS> </TS>

View File

@ -253,6 +253,17 @@ if(WIN32)
endif() endif()
endif() endif()
if(WITH_XC_YUBIKEY)
set(keepassx_SOURCES ${keepassx_SOURCES} gui/osutils/DeviceListener.cpp)
if(APPLE)
set(keepassx_SOURCES ${keepassx_SOURCES} gui/osutils/macutils/DeviceListenerMac.cpp)
elseif(UNIX)
set(keepassx_SOURCES ${keepassx_SOURCES} gui/osutils/nixutils/DeviceListenerLibUsb.cpp)
elseif(WIN32)
set(keepassx_SOURCES ${keepassx_SOURCES} gui/osutils/winutils/DeviceListenerWin.cpp)
endif()
endif()
set(keepassx_SOURCES ${keepassx_SOURCES} set(keepassx_SOURCES ${keepassx_SOURCES}
../share/icons/icons.qrc ../share/icons/icons.qrc
../share/wizard/wizard.qrc) ../share/wizard/wizard.qrc)
@ -401,7 +412,7 @@ if(HAIKU)
target_link_libraries(keepassx_core network) target_link_libraries(keepassx_core network)
endif() endif()
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
target_link_libraries(keepassx_core Qt5::DBus) target_link_libraries(keepassx_core Qt5::DBus ${LIBUSB_LIBRARIES})
if(WITH_XC_X11) if(WITH_XC_X11)
target_link_libraries(keepassx_core Qt5::X11Extras X11) target_link_libraries(keepassx_core Qt5::X11Extras X11)
endif() endif()

View File

@ -18,6 +18,8 @@
#ifndef KEEPASSXC_BOOTSTRAP_H #ifndef KEEPASSXC_BOOTSTRAP_H
#define KEEPASSXC_BOOTSTRAP_H #define KEEPASSXC_BOOTSTRAP_H
#include <QString>
namespace Bootstrap namespace Bootstrap
{ {
void bootstrap(); void bootstrap();

View File

@ -25,7 +25,6 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QTranslator> #include <QTranslator>
#include "config-keepassx.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/Resources.h" #include "core/Resources.h"

View File

@ -19,6 +19,7 @@
#define KEEPASSX_TRANSLATOR_H #define KEEPASSX_TRANSLATOR_H
#include <QMetaType> #include <QMetaType>
#include <QString>
class Translator class Translator
{ {

View File

@ -21,6 +21,7 @@
#define KEEPASSX_APPLICATION_H #define KEEPASSX_APPLICATION_H
#include <QApplication> #include <QApplication>
#include <QString>
#include <QtNetwork/qlocalserver.h> #include <QtNetwork/qlocalserver.h>
#if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)) #if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))

View File

@ -19,13 +19,15 @@
#include "DatabaseOpenWidget.h" #include "DatabaseOpenWidget.h"
#include "ui_DatabaseOpenWidget.h" #include "ui_DatabaseOpenWidget.h"
#include "config-keepassx.h"
#include "gui/FileDialog.h" #include "gui/FileDialog.h"
#include "gui/Icons.h" #include "gui/Icons.h"
#include "gui/MainWindow.h" #include "gui/MainWindow.h"
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include "keys/ChallengeResponseKey.h" #include "keys/ChallengeResponseKey.h"
#include "keys/FileKey.h" #include "keys/FileKey.h"
#ifdef WITH_XC_YUBIKEY
#include "keys/drivers/YubiKeyInterfaceUSB.h"
#endif
#include "quickunlock/QuickUnlockInterface.h" #include "quickunlock/QuickUnlockInterface.h"
#include <QCheckBox> #include <QCheckBox>
@ -58,6 +60,9 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
: DialogyWidget(parent) : DialogyWidget(parent)
, m_ui(new Ui::DatabaseOpenWidget()) , m_ui(new Ui::DatabaseOpenWidget())
, m_db(nullptr) , m_db(nullptr)
#ifdef WITH_XC_YUBIKEY
, m_deviceListener(new DeviceListener(this))
#endif
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
@ -90,18 +95,27 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase())); connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
m_ui->hardwareKeyLabelHelp->setIcon(icons()->icon("system-help").pixmap(QSize(12, 12))); connect(m_ui->addKeyFileLinkLabel, &QLabel::linkActivated, this, [&](const QString&) {
connect(m_ui->hardwareKeyLabelHelp, SIGNAL(clicked(bool)), SLOT(openHardwareKeyHelp())); if (browseKeyFile()) {
m_ui->keyFileLabelHelp->setIcon(icons()->icon("system-help").pixmap(QSize(12, 12))); toggleKeyFileComponent(true);
connect(m_ui->keyFileLabelHelp, SIGNAL(clicked(bool)), SLOT(openKeyFileHelp())); }
});
connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) {
if (text.isEmpty() && m_ui->keyFileLineEdit->isVisible()) {
toggleKeyFileComponent(false);
}
});
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
toggleKeyFileComponent(false);
toggleHardwareKeyComponent(false);
#ifdef WITH_XC_YUBIKEY
m_ui->hardwareKeyProgress->setVisible(false);
QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy(); QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy();
sp.setRetainSizeWhenHidden(true); sp.setRetainSizeWhenHidden(true);
m_ui->hardwareKeyProgress->setSizePolicy(sp); m_ui->hardwareKeyProgress->setSizePolicy(sp);
connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollHardwareKey())); #ifdef WITH_XC_YUBIKEY
connect(m_deviceListener, SIGNAL(devicePlugged(bool, void*, void*)), this, SLOT(pollHardwareKey()));
connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection); connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection);
connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] { connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] {
@ -113,12 +127,17 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
} }
}); });
connect(YubiKey::instance(), &YubiKey::challengeCompleted, this, [this] { m_ui->messageWidget->hide(); }); connect(YubiKey::instance(), &YubiKey::challengeCompleted, this, [this] { m_ui->messageWidget->hide(); });
m_ui->noHardwareKeysFoundLabel->setVisible(false);
m_ui->refreshHardwareKeys->setIcon(icons()->icon("yubikey-refresh", true));
connect(m_ui->refreshHardwareKeys, &QPushButton::clicked, this, [this] { pollHardwareKey(true); });
m_hideNoHardwareKeysFoundTimer.setInterval(2000);
connect(&m_hideNoHardwareKeysFoundTimer, &QTimer::timeout, this, [this] {
m_ui->noHardwareKeysFoundLabel->setVisible(false);
});
#else #else
m_ui->hardwareKeyLabel->setVisible(false); m_ui->noHardwareKeysFoundLabel->setVisible(false);
m_ui->hardwareKeyLabelHelp->setVisible(false); m_ui->refreshHardwareKeys->setVisible(false);
m_ui->buttonRedetectYubikey->setVisible(false);
m_ui->challengeResponseCombo->setVisible(false);
m_ui->hardwareKeyProgress->setVisible(false);
#endif #endif
// QuickUnlock actions // QuickUnlock actions
@ -129,6 +148,32 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
DatabaseOpenWidget::~DatabaseOpenWidget() = default; DatabaseOpenWidget::~DatabaseOpenWidget() = default;
void DatabaseOpenWidget::toggleKeyFileComponent(bool state)
{
m_ui->addKeyFileLinkLabel->setVisible(!state);
m_ui->selectKeyFileComponent->setVisible(state);
}
void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state)
{
m_ui->hardwareKeyProgress->setVisible(false);
m_ui->hardwareKeyComponent->setVisible(state);
m_ui->hardwareKeyCombo->setVisible(state && m_ui->hardwareKeyCombo->count() != 1);
m_ui->noHardwareKeysFoundLabel->setVisible(!state && m_manualHardwareKeyRefresh);
if (!state) {
m_ui->useHardwareKeyCheckBox->setChecked(false);
}
if (m_ui->hardwareKeyCombo->count() == 1) {
m_ui->useHardwareKeyCheckBox->setText(
tr("Use hardware key [Serial: %1]")
.arg(m_ui->hardwareKeyCombo->itemData(m_ui->hardwareKeyCombo->currentIndex())
.value<YubiKeySlot>()
.first));
} else {
m_ui->useHardwareKeyCheckBox->setText(tr("Use hardware key"));
}
}
void DatabaseOpenWidget::showEvent(QShowEvent* event) void DatabaseOpenWidget::showEvent(QShowEvent* event)
{ {
DialogyWidget::showEvent(event); DialogyWidget::showEvent(event);
@ -141,6 +186,24 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event)
m_ui->editPassword->setFocus(); m_ui->editPassword->setFocus();
} }
m_hideTimer.stop(); m_hideTimer.stop();
#ifdef WITH_XC_YUBIKEY
#ifdef Q_OS_WIN
m_deviceListener->registerHotplugCallback(true,
true,
YubiKeyInterfaceUSB::YUBICO_USB_VID,
DeviceListener::MATCH_ANY,
&DeviceListenerWin::DEV_CLS_KEYBOARD);
m_deviceListener->registerHotplugCallback(true,
true,
YubiKeyInterfaceUSB::ONLYKEY_USB_VID,
DeviceListener::MATCH_ANY,
&DeviceListenerWin::DEV_CLS_KEYBOARD);
#else
m_deviceListener->registerHotplugCallback(true, true, YubiKeyInterfaceUSB::YUBICO_USB_VID);
m_deviceListener->registerHotplugCallback(true, true, YubiKeyInterfaceUSB::ONLYKEY_USB_VID);
#endif
#endif
} }
void DatabaseOpenWidget::hideEvent(QHideEvent* event) void DatabaseOpenWidget::hideEvent(QHideEvent* event)
@ -151,6 +214,10 @@ void DatabaseOpenWidget::hideEvent(QHideEvent* event)
if (!isVisible()) { if (!isVisible()) {
m_hideTimer.start(); m_hideTimer.start();
} }
#ifdef WITH_XC_YUBIKEY
m_deviceListener->deregisterAllHotplugCallbacks();
#endif
} }
bool DatabaseOpenWidget::unlockingDatabase() bool DatabaseOpenWidget::unlockingDatabase()
@ -175,6 +242,7 @@ void DatabaseOpenWidget::load(const QString& filename)
auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash(); auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
if (lastKeyFiles.contains(m_filename)) { if (lastKeyFiles.contains(m_filename)) {
m_ui->keyFileLineEdit->setText(lastKeyFiles[m_filename].toString()); m_ui->keyFileLineEdit->setText(lastKeyFiles[m_filename].toString());
toggleKeyFileComponent(true);
} }
} }
@ -186,13 +254,8 @@ void DatabaseOpenWidget::load(const QString& filename)
} }
#ifdef WITH_XC_YUBIKEY #ifdef WITH_XC_YUBIKEY
// Only auto-poll for hardware keys if we previously used one with this database file // Do initial auto-poll
if (config()->get(Config::RememberLastKeyFiles).toBool()) { pollHardwareKey();
auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
if (lastChallengeResponse.contains(m_filename)) {
pollHardwareKey();
}
}
#endif #endif
} }
@ -204,7 +267,7 @@ void DatabaseOpenWidget::clearForms()
m_ui->keyFileLineEdit->clear(); m_ui->keyFileLineEdit->clear();
m_ui->keyFileLineEdit->setShowPassword(false); m_ui->keyFileLineEdit->setShowPassword(false);
m_ui->keyFileLineEdit->setClearButtonEnabled(true); m_ui->keyFileLineEdit->setClearButtonEnabled(true);
m_ui->challengeResponseCombo->clear(); m_ui->hardwareKeyCombo->clear();
m_ui->centralStack->setCurrentIndex(0); m_ui->centralStack->setCurrentIndex(0);
QString error; QString error;
@ -383,9 +446,9 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash(); auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
lastChallengeResponse.remove(m_filename); lastChallengeResponse.remove(m_filename);
int selectionIndex = m_ui->challengeResponseCombo->currentIndex(); int selectionIndex = m_ui->hardwareKeyCombo->currentIndex();
if (selectionIndex > 0) { if (m_ui->useHardwareKeyCheckBox->isChecked()) {
auto slot = m_ui->challengeResponseCombo->itemData(selectionIndex).value<YubiKeySlot>(); auto slot = m_ui->hardwareKeyCombo->itemData(selectionIndex).value<YubiKeySlot>();
auto crKey = QSharedPointer<ChallengeResponseKey>(new ChallengeResponseKey(slot)); auto crKey = QSharedPointer<ChallengeResponseKey>(new ChallengeResponseKey(slot));
databaseKey->addChallengeResponseKey(crKey); databaseKey->addChallengeResponseKey(crKey);
@ -406,55 +469,65 @@ void DatabaseOpenWidget::reject()
emit dialogFinished(false); emit dialogFinished(false);
} }
void DatabaseOpenWidget::browseKeyFile() bool DatabaseOpenWidget::browseKeyFile()
{ {
QString filters = QString("%1 (*);;%2 (*.keyx; *.key)").arg(tr("All files"), tr("Key files")); QString filters = QString("%1 (*);;%2 (*.keyx; *.key)").arg(tr("All files"), tr("Key files"));
QString filename = fileDialog()->getOpenFileName(this, tr("Select key file"), QString(), filters); QString filename =
fileDialog()->getOpenFileName(this, tr("Select key file"), FileDialog::getLastDir("keyfile"), filters);
if (filename.isEmpty()) {
return false;
}
FileDialog::saveLastDir("keyfile", filename, true);
if (QFileInfo(filename).canonicalFilePath() == QFileInfo(m_filename).canonicalFilePath()) { if (QFileInfo(filename).canonicalFilePath() == QFileInfo(m_filename).canonicalFilePath()) {
MessageBox::warning(this, MessageBox::warning(this,
tr("Cannot use database file as key file"), tr("Cannot use database file as key file"),
tr("You cannot use your database file as a key file.\nIf you do not have a key file, " tr("Your database file is NOT a key file!\nIf you don't have a key file or don't know what "
"please leave the field empty."), "that is, you don't have to select one."),
MessageBox::Button::Ok); MessageBox::Button::Ok);
filename = ""; return false;
}
if (filename.endsWith(".kdbx")
&& MessageBox::warning(this,
tr("KeePassXC database file selected"),
tr("The file you selected looks like a database file.\nA database file is NOT a key "
"file!\n\nAre you sure you want to continue with this file?."),
MessageBox::Button::Yes | MessageBox::Button::Cancel,
MessageBox::Button::Cancel)
!= MessageBox::Yes) {
return false;
} }
if (!filename.isEmpty()) { m_ui->keyFileLineEdit->setText(filename);
m_ui->keyFileLineEdit->setText(filename); return true;
}
} }
void DatabaseOpenWidget::pollHardwareKey() void DatabaseOpenWidget::pollHardwareKey(bool manualTrigger)
{ {
if (m_pollingHardwareKey) { if (m_pollingHardwareKey) {
return; return;
} }
m_ui->challengeResponseCombo->clear(); m_ui->hardwareKeyCombo->setEnabled(false);
m_ui->challengeResponseCombo->addItem(tr("Detecting hardware keys…"));
m_ui->buttonRedetectYubikey->setEnabled(false);
m_ui->challengeResponseCombo->setEnabled(false);
m_ui->hardwareKeyProgress->setVisible(true); m_ui->hardwareKeyProgress->setVisible(true);
m_ui->refreshHardwareKeys->setEnabled(false);
m_ui->noHardwareKeysFoundLabel->setVisible(false);
m_pollingHardwareKey = true; m_pollingHardwareKey = true;
m_manualHardwareKeyRefresh = manualTrigger;
YubiKey::instance()->findValidKeysAsync(); YubiKey::instance()->findValidKeysAsync();
} }
void DatabaseOpenWidget::hardwareKeyResponse(bool found) void DatabaseOpenWidget::hardwareKeyResponse(bool found)
{ {
m_ui->challengeResponseCombo->clear();
m_ui->buttonRedetectYubikey->setEnabled(true);
m_ui->hardwareKeyProgress->setVisible(false); m_ui->hardwareKeyProgress->setVisible(false);
m_ui->refreshHardwareKeys->setEnabled(true);
m_ui->hardwareKeyCombo->clear();
m_pollingHardwareKey = false; m_pollingHardwareKey = false;
if (!found) { if (!found) {
m_ui->challengeResponseCombo->addItem(tr("No hardware keys detected")); toggleHardwareKeyComponent(false);
m_ui->challengeResponseCombo->setEnabled(false);
return; return;
} else {
m_ui->challengeResponseCombo->addItem(tr("Select hardware key…"));
} }
YubiKeySlot lastUsedSlot; YubiKeySlot lastUsedSlot;
@ -466,31 +539,24 @@ void DatabaseOpenWidget::hardwareKeyResponse(bool found)
if (split.size() > 1) { if (split.size() > 1) {
lastUsedSlot = YubiKeySlot(split[0].toUInt(), split[1].toInt()); lastUsedSlot = YubiKeySlot(split[0].toUInt(), split[1].toInt());
} }
m_ui->useHardwareKeyCheckBox->setChecked(true);
} }
} }
int selectedIndex = 0; int selectedIndex = 0;
for (auto& slot : YubiKey::instance()->foundKeys()) { const auto foundKeys = YubiKey::instance()->foundKeys();
for (auto i = foundKeys.cbegin(); i != foundKeys.cend(); ++i) {
// add detected YubiKey to combo box // add detected YubiKey to combo box
m_ui->challengeResponseCombo->addItem(YubiKey::instance()->getDisplayName(slot), QVariant::fromValue(slot)); m_ui->hardwareKeyCombo->addItem(i.value(), QVariant::fromValue(i.key()));
// Select this YubiKey + Slot if we used it in the past // Select this YubiKey + Slot if we used it in the past
if (lastUsedSlot == slot) { if (lastUsedSlot == i.key()) {
selectedIndex = m_ui->challengeResponseCombo->count() - 1; selectedIndex = m_ui->hardwareKeyCombo->count() - 1;
} }
} }
m_ui->challengeResponseCombo->setCurrentIndex(selectedIndex); toggleHardwareKeyComponent(true);
m_ui->challengeResponseCombo->setEnabled(true); m_ui->hardwareKeyCombo->setEnabled(m_ui->useHardwareKeyCheckBox->isChecked());
} m_ui->hardwareKeyCombo->setCurrentIndex(selectedIndex);
void DatabaseOpenWidget::openHardwareKeyHelp()
{
QDesktopServices::openUrl(QUrl("https://keepassxc.org/docs/#faq-yubikey-2fa"));
}
void DatabaseOpenWidget::openKeyFileHelp()
{
QDesktopServices::openUrl(QUrl("https://keepassxc.org/docs/#faq-keyfile-howto"));
} }
void DatabaseOpenWidget::setUserInteractionLock(bool state) void DatabaseOpenWidget::setUserInteractionLock(bool state)

View File

@ -19,10 +19,15 @@
#ifndef KEEPASSX_DATABASEOPENWIDGET_H #ifndef KEEPASSX_DATABASEOPENWIDGET_H
#define KEEPASSX_DATABASEOPENWIDGET_H #define KEEPASSX_DATABASEOPENWIDGET_H
#include <QPointer>
#include <QScopedPointer> #include <QScopedPointer>
#include <QTimer> #include <QTimer>
#include "config-keepassx.h"
#include "gui/DialogyWidget.h" #include "gui/DialogyWidget.h"
#ifdef WITH_XC_YUBIKEY
#include "osutils/DeviceListener.h"
#endif
class CompositeKey; class CompositeKey;
class Database; class Database;
@ -71,17 +76,22 @@ protected slots:
void reject(); void reject();
private slots: private slots:
void browseKeyFile(); bool browseKeyFile();
void pollHardwareKey(); void toggleKeyFileComponent(bool state);
void toggleHardwareKeyComponent(bool state);
void pollHardwareKey(bool manualTrigger = false);
void hardwareKeyResponse(bool found); void hardwareKeyResponse(bool found);
void openHardwareKeyHelp();
void openKeyFileHelp();
private: private:
#ifdef WITH_XC_YUBIKEY
QPointer<DeviceListener> m_deviceListener;
#endif
bool m_pollingHardwareKey = false; bool m_pollingHardwareKey = false;
bool m_manualHardwareKeyRefresh = false;
bool m_blockQuickUnlock = false; bool m_blockQuickUnlock = false;
bool m_unlockingDatabase = false; bool m_unlockingDatabase = false;
QTimer m_hideTimer; QTimer m_hideTimer;
QTimer m_hideNoHardwareKeysFoundTimer;
Q_DISABLE_COPY(DatabaseOpenWidget) Q_DISABLE_COPY(DatabaseOpenWidget)
}; };

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>520</width> <width>745</width>
<height>436</height> <height>544</height>
</rect> </rect>
</property> </property>
<property name="accessibleName"> <property name="accessibleName">
@ -18,7 +18,7 @@
<widget class="MessageWidget" name="messageWidget" native="true"/> <widget class="MessageWidget" name="messageWidget" native="true"/>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1,0"> <layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0,1">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
</property> </property>
@ -40,18 +40,6 @@
</item> </item>
<item> <item>
<widget class="QWidget" name="formContainer" native="true"> <widget class="QWidget" name="formContainer" native="true">
<property name="minimumSize">
<size>
<width>500</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>700</width>
<height>16777215</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="1,0,0,0,0,2"> <layout class="QVBoxLayout" name="verticalLayout_4" stretch="1,0,0,0,0,2">
<item> <item>
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
@ -71,7 +59,6 @@
<property name="font"> <property name="font">
<font> <font>
<pointsize>12</pointsize> <pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold> <bold>true</bold>
</font> </font>
</property> </property>
@ -107,8 +94,8 @@
<widget class="QStackedWidget" name="centralStack"> <widget class="QStackedWidget" name="centralStack">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>650</width>
<height>250</height> <height>0</height>
</size> </size>
</property> </property>
<property name="frameShape"> <property name="frameShape">
@ -122,172 +109,223 @@
</property> </property>
<widget class="QWidget" name="mainPage"> <widget class="QWidget" name="mainPage">
<layout class="QVBoxLayout" name="verticalLayout_6"> <layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin"> <property name="leftMargin">
<number>20</number> <number>30</number>
</property> </property>
<property name="topMargin"> <property name="topMargin">
<number>15</number> <number>25</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>20</number> <number>30</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>15</number> <number>25</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="label"> <widget class="QFrame" name="enterPasswordComponent">
<property name="text"> <property name="lineWidth">
<string>Enter Password:</string> <number>0</number>
</property>
<property name="buddy">
<cstring>editPassword</cstring>
</property> </property>
<layout class="QVBoxLayout" name="enterPasswordComponentLayout">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Enter Password:</string>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="PasswordWidget" name="editPassword" native="true">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Password field</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="hardwareKeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>4</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="PasswordWidget" name="editPassword" native="true"> <widget class="QFrame" name="selectKeyFileComponent">
<property name="focusPolicy"> <property name="frameShape">
<enum>Qt::StrongFocus</enum> <enum>QFrame::NoFrame</enum>
</property> </property>
<property name="accessibleName"> <property name="frameShadow">
<string>Password field</string> <enum>QFrame::Plain</enum>
</property> </property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="selectKeyFileComponentLayout">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<widget class="QLabel" name="selectKeyFileLabel">
<property name="text">
<string>Select Key File:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7" stretch="1,0">
<item>
<widget class="PasswordWidget" name="keyFileLineEdit" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Key file to unlock the database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonBrowseFile">
<property name="toolTip">
<string>Browse for key file</string>
</property>
<property name="accessibleName">
<string>Browse for key file</string>
</property>
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget> </widget>
</item> </item>
<item> <item>
<spacer name="verticalSpacer_4"> <widget class="QFrame" name="addAdditionalKeysComponent">
<property name="orientation"> <property name="lineWidth">
<enum>Qt::Vertical</enum> <number>0</number>
</property> </property>
<property name="sizeType"> <layout class="QHBoxLayout" name="addAdditionalKeysComponentLayout">
<enum>QSizePolicy::Fixed</enum> <property name="sizeConstraint">
</property> <enum>QLayout::SetMinimumSize</enum>
<property name="sizeHint" stdset="0"> </property>
<size> <property name="leftMargin">
<width>20</width> <number>0</number>
<height>5</height> </property>
</size> <property name="topMargin">
</property> <number>0</number>
</spacer> </property>
</item> <property name="rightMargin">
<item> <number>0</number>
<widget class="QLabel" name="label_2"> </property>
<property name="text"> <property name="bottomMargin">
<string>Enter Additional Credentials (if any):</string> <number>0</number>
</property> </property>
</widget> <item>
</item> <widget class="QFrame" name="hardwareKeyComponent">
<item> <layout class="QHBoxLayout" name="hardwareKeyComponentLayout">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>15</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<property name="topMargin">
<number>3</number>
</property>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing"> <property name="spacing">
<number>5</number> <number>5</number>
</property> </property>
<item> <property name="leftMargin">
<widget class="QLabel" name="keyFileLabel">
<property name="text">
<string>Key File:</string>
</property>
<property name="buddy">
<cstring>keyFileLineEdit</cstring>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="keyFileLabelHelp">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="toolTip">
<string>&lt;p&gt;In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database's security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;br&gt;If you do not have a key file, leave this field empty.&lt;/p&gt;&lt;p&gt;Click for more information…&lt;/p&gt;</string>
</property>
<property name="accessibleName">
<string>Key file help</string>
</property>
<property name="styleSheet">
<string notr="true">QToolButton {
border: none;
background: none;
}</string>
</property>
<property name="text">
<string notr="true">?</string>
</property>
<property name="iconSize">
<size>
<width>12</width>
<height>12</height>
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="3">
<layout class="QGridLayout" name="gridLayout">
<property name="spacing">
<number>0</number> <number>0</number>
</property> </property>
<item row="1" column="2"> <property name="topMargin">
<widget class="QProgressBar" name="hardwareKeyProgress"> <number>0</number>
<property name="maximumSize"> </property>
<size> <property name="rightMargin">
<width>16777215</width> <number>0</number>
<height>2</height> </property>
</size> <property name="bottomMargin">
</property> <number>0</number>
<property name="minimum"> </property>
<number>0</number> <item>
</property> <widget class="QCheckBox" name="useHardwareKeyCheckBox">
<property name="maximum"> <property name="text">
<number>0</number> <string notr="true">Use Hardware Security Key [Serial: 11111111]</string>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="2"> <item>
<widget class="QComboBox" name="challengeResponseCombo"> <widget class="QComboBox" name="hardwareKeyCombo">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="sizePolicy"> <property name="minimumSize">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <size>
<horstretch>0</horstretch> <width>200</width>
<verstretch>0</verstretch> <height>0</height>
</sizepolicy> </size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>16777215</height>
</size>
</property> </property>
<property name="accessibleName"> <property name="accessibleName">
<string>Hardware key slot selection</string> <string>Hardware key slot selection</string>
@ -298,165 +336,110 @@
</widget> </widget>
</item> </item>
</layout> </layout>
</item> </widget>
<item row="1" column="0"> </item>
<layout class="QVBoxLayout" name="verticalLayout_7"> <item>
<property name="spacing"> <widget class="QLabel" name="noHardwareKeysFoundLabel">
<number>2</number> <property name="sizePolicy">
</property> <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<item> <horstretch>0</horstretch>
<layout class="QHBoxLayout" name="horizontalLayout_6"> <verstretch>0</verstretch>
<property name="spacing"> </sizepolicy>
<number>5</number> </property>
</property> <property name="text">
<item> <string>No hardware keys found.</string>
<widget class="QLabel" name="hardwareKeyLabel"> </property>
<property name="text"> <property name="margin">
<string>Hardware Key:</string> <number>1</number>
</property> </property>
<property name="buddy"> </widget>
<cstring>challengeResponseCombo</cstring> </item>
</property> <item>
</widget> <spacer name="horizontalSpacer">
</item> <property name="orientation">
<item> <enum>Qt::Horizontal</enum>
<widget class="QToolButton" name="hardwareKeyLabelHelp"> </property>
<property name="cursor"> <property name="sizeHint" stdset="0">
<cursorShape>PointingHandCursor</cursorShape> <size>
</property> <width>40</width>
<property name="focusPolicy"> <height>0</height>
<enum>Qt::ClickFocus</enum> </size>
</property> </property>
<property name="toolTip"> </spacer>
<string>&lt;p&gt;You can use a hardware security key such as a &lt;strong&gt;YubiKey&lt;/strong&gt; or &lt;strong&gt;OnlyKey&lt;/strong&gt; with slots configured for HMAC-SHA1.&lt;/p&gt; </item>
&lt;p&gt;Click for more information…&lt;/p&gt;</string> <item>
</property> <widget class="QPushButton" name="refreshHardwareKeys">
<property name="accessibleName"> <property name="cursor">
<string>Hardware key help</string> <cursorShape>PointingHandCursor</cursorShape>
</property> </property>
<property name="styleSheet"> <property name="toolTip">
<string notr="true">QToolButton { <string>Refresh Hardware Keys</string>
border: none; </property>
background: none; <property name="accessibleName">
}</string> <string>Refresh Hardware Keys</string>
</property> </property>
<property name="text"> <property name="styleSheet">
<string notr="true">?</string> <string notr="true">QPushButton { background-color: transparent; border: none; } </string>
</property> </property>
<property name="iconSize"> <property name="text">
<size> <string notr="true"/>
<width>12</width> </property>
<height>12</height> </widget>
</size> </item>
</property> <item>
<property name="popupMode"> <widget class="QLabel" name="addKeyFileLinkLabel">
<enum>QToolButton::InstantPopup</enum> <property name="cursor">
</property> <cursorShape>PointingHandCursor</cursorShape>
</widget> </property>
</item> <property name="focusPolicy">
</layout> <enum>Qt::TabFocus</enum>
</item> </property>
<item> <property name="toolTip">
<spacer name="verticalSpacer_6"> <string>&lt;p&gt;In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database's security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;/p&gt;</string>
<property name="orientation"> </property>
<enum>Qt::Vertical</enum> <property name="accessibleDescription">
</property> <string>Click to add a key file.</string>
<property name="sizeType"> </property>
<enum>QSizePolicy::Fixed</enum> <property name="text">
</property> <string>&lt;a href=&quot;#&quot; style=&quot;text-decoration: underline&quot;&gt;I have a key file&lt;/a&gt;</string>
<property name="sizeHint" stdset="0"> </property>
<size> <property name="alignment">
<width>0</width> <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
<height>2</height> </property>
</size> <property name="margin">
</property> <number>1</number>
</spacer> </property>
</item> <property name="textInteractionFlags">
</layout> <set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</item> </property>
<item row="0" column="3"> </widget>
<layout class="QGridLayout" name="gridLayout_2"> </item>
<property name="verticalSpacing"> </layout>
<number>0</number> </widget>
</property> </item>
<item row="0" column="1"> <item>
<widget class="PasswordWidget" name="keyFileLineEdit" native="true"> <spacer name="verticalSpacer_4">
<property name="sizePolicy"> <property name="orientation">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <enum>Qt::Vertical</enum>
<horstretch>0</horstretch> </property>
<verstretch>0</verstretch> <property name="sizeHint" stdset="0">
</sizepolicy> <size>
</property> <width>0</width>
<property name="focusPolicy"> <height>5</height>
<enum>Qt::StrongFocus</enum> </size>
</property> </property>
<property name="accessibleName"> </spacer>
<string>Key file to unlock the database</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="buttonBrowseFile">
<property name="toolTip">
<string>Browse for key file</string>
</property>
<property name="accessibleName">
<string>Browse for key file</string>
</property>
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
<item row="1" column="4">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="buttonRedetectYubikey">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Refresh hardware tokens</string>
</property>
<property name="accessibleName">
<string>Refresh hardware tokens</string>
</property>
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>2</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item> </item>
<item> <item>
<layout class="QHBoxLayout" name="dialogButtonsLayout"> <layout class="QHBoxLayout" name="dialogButtonsLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin"> <property name="topMargin">
<number>15</number> <number>25</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property> </property>
<item alignment="Qt::AlignRight"> <item alignment="Qt::AlignRight">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
@ -474,17 +457,20 @@
</widget> </widget>
<widget class="QWidget" name="quickUnlockPage"> <widget class="QWidget" name="quickUnlockPage">
<layout class="QHBoxLayout" name="horizontalLayout_2"> <layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin"> <property name="leftMargin">
<number>20</number> <number>10</number>
</property> </property>
<property name="topMargin"> <property name="topMargin">
<number>15</number> <number>10</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>20</number> <number>10</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>15</number> <number>10</number>
</property> </property>
<item> <item>
<spacer name="horizontalSpacer_5"> <spacer name="horizontalSpacer_5">
@ -493,8 +479,8 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -508,8 +494,8 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>0</width>
<height>40</height> <height>10</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -525,7 +511,6 @@
<property name="font"> <property name="font">
<font> <font>
<pointsize>10</pointsize> <pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold> <bold>true</bold>
</font> </font>
</property> </property>
@ -551,8 +536,8 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>0</width>
<height>40</height> <height>10</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -566,8 +551,8 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -635,13 +620,16 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>editPassword</tabstop> <tabstop>editPassword</tabstop>
<tabstop>keyFileLineEdit</tabstop> <tabstop>keyFileLineEdit</tabstop>
<tabstop>buttonBrowseFile</tabstop> <tabstop>buttonBrowseFile</tabstop>
<tabstop>challengeResponseCombo</tabstop> <tabstop>useHardwareKeyCheckBox</tabstop>
<tabstop>buttonRedetectYubikey</tabstop> <tabstop>hardwareKeyCombo</tabstop>
<tabstop>quickUnlockButton</tabstop> <tabstop>refreshHardwareKeys</tabstop>
<tabstop>resetQuickUnlockButton</tabstop> <tabstop>addKeyFileLinkLabel</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <connections/>

View File

@ -16,21 +16,30 @@
*/ */
#include "YubiKeyEditWidget.h" #include "YubiKeyEditWidget.h"
#include "ui_KeyComponentWidget.h" #include "ui_KeyComponentWidget.h"
#include "ui_YubiKeyEditWidget.h" #include "ui_YubiKeyEditWidget.h"
#include "config-keepassx.h"
#include "core/AsyncTask.h" #include "core/AsyncTask.h"
#include "gui/Icons.h"
#include "keys/ChallengeResponseKey.h" #include "keys/ChallengeResponseKey.h"
#include "keys/CompositeKey.h" #include "keys/CompositeKey.h"
#ifdef WITH_XC_YUBIKEY
#include "keys/drivers/YubiKeyInterfaceUSB.h"
#endif
YubiKeyEditWidget::YubiKeyEditWidget(QWidget* parent) YubiKeyEditWidget::YubiKeyEditWidget(QWidget* parent)
: KeyComponentWidget(parent) : KeyComponentWidget(parent)
, m_compUi(new Ui::YubiKeyEditWidget()) , m_compUi(new Ui::YubiKeyEditWidget())
#ifdef WITH_XC_YUBIKEY
, m_deviceListener(new DeviceListener(this))
#endif
{ {
initComponent(); initComponent();
#ifdef WITH_XC_YUBIKEY
connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection); connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection);
connect(m_deviceListener, &DeviceListener::devicePlugged, this, [&](bool, void*, void*) { pollYubikey(); });
#endif
} }
YubiKeyEditWidget::~YubiKeyEditWidget() = default; YubiKeyEditWidget::~YubiKeyEditWidget() = default;
@ -74,19 +83,48 @@ QWidget* YubiKeyEditWidget::componentEditWidget()
m_compUi->yubikeyProgress->setSizePolicy(sp); m_compUi->yubikeyProgress->setSizePolicy(sp);
m_compUi->yubikeyProgress->setVisible(false); m_compUi->yubikeyProgress->setVisible(false);
#ifdef WITH_XC_YUBIKEY
connect(m_compUi->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey()));
pollYubikey();
#endif
return m_compEditWidget; return m_compEditWidget;
} }
void YubiKeyEditWidget::showEvent(QShowEvent* event)
{
KeyComponentWidget::showEvent(event);
#ifdef WITH_XC_YUBIKEY
#ifdef Q_OS_WIN
m_deviceListener->registerHotplugCallback(true,
true,
YubiKeyInterfaceUSB::YUBICO_USB_VID,
DeviceListener::MATCH_ANY,
&DeviceListenerWin::DEV_CLS_KEYBOARD);
m_deviceListener->registerHotplugCallback(true,
true,
YubiKeyInterfaceUSB::ONLYKEY_USB_VID,
DeviceListener::MATCH_ANY,
&DeviceListenerWin::DEV_CLS_KEYBOARD);
#else
m_deviceListener->registerHotplugCallback(true, true, YubiKeyInterfaceUSB::YUBICO_USB_VID);
m_deviceListener->registerHotplugCallback(true, true, YubiKeyInterfaceUSB::ONLYKEY_USB_VID);
#endif
#endif
}
void YubiKeyEditWidget::hideEvent(QHideEvent* event)
{
KeyComponentWidget::hideEvent(event);
#ifdef WITH_XC_YUBIKEY
m_deviceListener->deregisterAllHotplugCallbacks();
#endif
}
void YubiKeyEditWidget::initComponentEditWidget(QWidget* widget) void YubiKeyEditWidget::initComponentEditWidget(QWidget* widget)
{ {
Q_UNUSED(widget); Q_UNUSED(widget);
Q_ASSERT(m_compEditWidget); Q_ASSERT(m_compEditWidget);
m_compUi->comboChallengeResponse->setFocus(); m_compUi->comboChallengeResponse->setFocus();
m_compUi->refreshHardwareKeys->setIcon(icons()->icon("yubikey-refresh", true));
connect(m_compUi->refreshHardwareKeys, &QPushButton::clicked, this, &YubiKeyEditWidget::pollYubikey);
pollYubikey();
} }
void YubiKeyEditWidget::initComponent() void YubiKeyEditWidget::initComponent()
@ -116,9 +154,9 @@ void YubiKeyEditWidget::pollYubikey()
m_isDetected = false; m_isDetected = false;
m_compUi->comboChallengeResponse->clear(); m_compUi->comboChallengeResponse->clear();
m_compUi->comboChallengeResponse->addItem(tr("Detecting hardware keys…")); m_compUi->comboChallengeResponse->addItem(tr("Detecting hardware keys…"));
m_compUi->buttonRedetectYubikey->setEnabled(false);
m_compUi->comboChallengeResponse->setEnabled(false); m_compUi->comboChallengeResponse->setEnabled(false);
m_compUi->yubikeyProgress->setVisible(true); m_compUi->yubikeyProgress->setVisible(true);
m_compUi->refreshHardwareKeys->setEnabled(false);
YubiKey::instance()->findValidKeysAsync(); YubiKey::instance()->findValidKeysAsync();
#endif #endif
@ -131,20 +169,22 @@ void YubiKeyEditWidget::hardwareKeyResponse(bool found)
} }
m_compUi->comboChallengeResponse->clear(); m_compUi->comboChallengeResponse->clear();
m_compUi->buttonRedetectYubikey->setEnabled(true); m_compUi->refreshHardwareKeys->setEnabled(true);
m_compUi->yubikeyProgress->setVisible(false);
if (!found) { if (!found) {
m_compUi->yubikeyProgress->setVisible(false);
m_compUi->comboChallengeResponse->addItem(tr("No hardware keys detected")); m_compUi->comboChallengeResponse->addItem(tr("No hardware keys detected"));
m_isDetected = false; m_isDetected = false;
return; return;
} }
for (auto& slot : YubiKey::instance()->foundKeys()) { const auto foundKeys = YubiKey::instance()->foundKeys();
for (auto i = foundKeys.cbegin(); i != foundKeys.cend(); ++i) {
// add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB // add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB
m_compUi->comboChallengeResponse->addItem(YubiKey::instance()->getDisplayName(slot), QVariant::fromValue(slot)); m_compUi->comboChallengeResponse->addItem(i.value(), QVariant::fromValue(i.key()));
} }
m_isDetected = true; m_isDetected = true;
m_compUi->yubikeyProgress->setVisible(false);
m_compUi->comboChallengeResponse->setEnabled(true); m_compUi->comboChallengeResponse->setEnabled(true);
} }

View File

@ -19,6 +19,10 @@
#define KEEPASSXC_YUBIKEYEDITWIDGET_H #define KEEPASSXC_YUBIKEYEDITWIDGET_H
#include "KeyComponentWidget.h" #include "KeyComponentWidget.h"
#include "config-keepassx.h"
#ifdef WITH_XC_YUBIKEY
#include "gui/osutils/DeviceListener.h"
#endif
namespace Ui namespace Ui
{ {
@ -43,6 +47,8 @@ protected:
QWidget* componentEditWidget() override; QWidget* componentEditWidget() override;
void initComponentEditWidget(QWidget* widget) override; void initComponentEditWidget(QWidget* widget) override;
void initComponent() override; void initComponent() override;
void showEvent(QShowEvent* event) override;
void hideEvent(QHideEvent* event) override;
private slots: private slots:
void hardwareKeyResponse(bool found); void hardwareKeyResponse(bool found);
@ -51,6 +57,9 @@ private slots:
private: private:
const QScopedPointer<Ui::YubiKeyEditWidget> m_compUi; const QScopedPointer<Ui::YubiKeyEditWidget> m_compUi;
QPointer<QWidget> m_compEditWidget; QPointer<QWidget> m_compEditWidget;
#ifdef WITH_XC_YUBIKEY
QPointer<DeviceListener> m_deviceListener;
#endif
bool m_isDetected = false; bool m_isDetected = false;
}; };

View File

@ -24,51 +24,84 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<layout class="QGridLayout" name="gridLayout_4"> <layout class="QHBoxLayout" name="horizontalLayout">
<property name="verticalSpacing"> <property name="spacing">
<number>0</number> <number>6</number>
</property> </property>
<item row="0" column="1"> <item>
<widget class="QPushButton" name="buttonRedetectYubikey"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="accessibleName"> <property name="spacing">
<string>Refresh hardware tokens</string>
</property>
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="comboChallengeResponse">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Hardware key slot selection</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QProgressBar" name="yubikeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
</size>
</property>
<property name="maximum">
<number>0</number> <number>0</number>
</property> </property>
<property name="value"> <item>
<number>-1</number> <widget class="QComboBox" name="comboChallengeResponse">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Hardware key slot selection</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="yubikeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
</size>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property> </property>
<property name="textVisible"> <item>
<bool>false</bool> <widget class="QPushButton" name="refreshHardwareKeys">
</property> <property name="toolTip">
</widget> <string>Refresh hardware keys</string>
</property>
<property name="accessibleName">
<string>Refresh hardware keys</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>2</height>
</size>
</property>
</spacer>
</item>
</layout>
</item> </item>
</layout> </layout>
</item> </item>
@ -87,10 +120,6 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<tabstops>
<tabstop>comboChallengeResponse</tabstop>
<tabstop>buttonRedetectYubikey</tabstop>
</tabstops>
<resources/> <resources/>
<connections/> <connections/>
</ui> </ui>

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "DeviceListener.h"
#include <QTimer>
DeviceListener::DeviceListener(QWidget* parent)
: QWidget(parent)
{
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
m_listeners[0] = new DEVICELISTENER_IMPL(this);
connectSignals(m_listeners[0]);
#endif
}
DeviceListener::~DeviceListener()
{
}
void DeviceListener::connectSignals(DEVICELISTENER_IMPL* listener)
{
connect(listener, &DEVICELISTENER_IMPL::devicePlugged, this, [&](bool state, void* ctx, void* device) {
// Wait a few ms to prevent USB device access conflicts
QTimer::singleShot(50, [&] { emit devicePlugged(state, ctx, device); });
});
}
DeviceListener::Handle
DeviceListener::registerHotplugCallback(bool arrived, bool left, int vendorId, int productId, const QUuid* deviceClass)
{
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
const Handle handle = m_listeners[0]->registerHotplugCallback(arrived, left, vendorId, productId, deviceClass);
#else
auto* listener = new DEVICELISTENER_IMPL(this);
const auto handle = reinterpret_cast<Handle>(listener);
m_listeners[handle] = listener;
m_listeners[handle]->registerHotplugCallback(arrived, left, vendorId, productId, deviceClass);
connectSignals(m_listeners[handle]);
#endif
return handle;
}
void DeviceListener::deregisterHotplugCallback(Handle handle)
{
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
m_listeners[0]->deregisterHotplugCallback(static_cast<int>(handle));
#else
if (m_listeners.contains(handle)) {
m_listeners[handle]->deregisterHotplugCallback();
m_listeners.remove(handle);
}
#endif
}
void DeviceListener::deregisterAllHotplugCallbacks()
{
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)
m_listeners[0]->deregisterAllHotplugCallbacks();
#else
while (!m_listeners.isEmpty()) {
deregisterHotplugCallback(m_listeners.constBegin().key());
}
#endif
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICELISTENER_H
#define DEVICELISTENER_H
#include <QHash>
#include <QPair>
#include <QPointer>
#include <QWidget>
#if defined(Q_OS_WIN)
#include "winutils/DeviceListenerWin.h"
#elif defined(Q_OS_MACOS)
#include "macutils/DeviceListenerMac.h"
#elif defined(Q_OS_UNIX)
#include "nixutils/DeviceListenerLibUsb.h"
#endif
class QUuid;
class DeviceListener : public QWidget
{
Q_OBJECT
public:
typedef qintptr Handle;
static constexpr int MATCH_ANY = -1;
explicit DeviceListener(QWidget* parent);
DeviceListener(const DeviceListener&) = delete;
~DeviceListener() override;
/**
* Register a hotplug notification callback.
*
* Fires devicePlugged() or deviceUnplugged() when the state of a matching device changes.
* The signals are supplied with the platform-specific context and ID of the firing device.
* Registering a new callback with the same DeviceListener will unregister any previous callbacks.
*
* @param arrived listen for new devices
* @param left listen for device unplug
* @param vendorId vendor ID to listen for or DeviceListener::MATCH_ANY
* @param productId product ID to listen for or DeviceListener::MATCH_ANY
* @param deviceClass device class GUID (Windows only)
* @return callback handle
*/
Handle registerHotplugCallback(bool arrived,
bool left,
int vendorId = MATCH_ANY,
int productId = MATCH_ANY,
const QUuid* deviceClass = nullptr);
void deregisterHotplugCallback(Handle handle);
void deregisterAllHotplugCallbacks();
signals:
void devicePlugged(bool state, void* ctx, void* device);
private:
QHash<Handle, QPointer<DEVICELISTENER_IMPL>> m_listeners;
void connectSignals(DEVICELISTENER_IMPL* listener);
};
#endif // DEVICELISTENER_H

View File

@ -26,7 +26,7 @@ class ScreenLockListener : public QObject
Q_OBJECT Q_OBJECT
public: public:
ScreenLockListener(QWidget* parent = nullptr); explicit ScreenLockListener(QWidget* parent);
~ScreenLockListener() override; ~ScreenLockListener() override;
signals: signals:

View File

@ -17,7 +17,7 @@
#ifndef SCREENLOCKLISTENERPRIVATE_H #ifndef SCREENLOCKLISTENERPRIVATE_H
#define SCREENLOCKLISTENERPRIVATE_H #define SCREENLOCKLISTENERPRIVATE_H
#include <QObject> #include <QWidget>
class ScreenLockListenerPrivate : public QObject class ScreenLockListenerPrivate : public QObject
{ {
@ -26,7 +26,7 @@ public:
static ScreenLockListenerPrivate* instance(QWidget* parent = nullptr); static ScreenLockListenerPrivate* instance(QWidget* parent = nullptr);
protected: protected:
ScreenLockListenerPrivate(QWidget* parent = nullptr); explicit ScreenLockListenerPrivate(QWidget* parent = nullptr);
signals: signals:
void screenLocked(); void screenLocked();

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "DeviceListenerMac.h"
#include <QPointer>
#include <IOKit/IOKitLib.h>
DeviceListenerMac::DeviceListenerMac(QObject* parent)
: QObject(parent)
, m_mgr(nullptr)
{
}
DeviceListenerMac::~DeviceListenerMac()
{
if (m_mgr) {
IOHIDManagerUnscheduleFromRunLoop(m_mgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
IOHIDManagerClose(m_mgr, kIOHIDOptionsTypeNone);
CFRelease(m_mgr);
}
}
void DeviceListenerMac::registerHotplugCallback(bool arrived, bool left, int vendorId, int productId, const QUuid*)
{
if (!m_mgr) {
m_mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone);
if (!m_mgr) {
qWarning("Failed to create IOHIDManager.");
return;
}
IOHIDManagerScheduleWithRunLoop(m_mgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
}
if (vendorId > 0 || productId > 0) {
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOHIDDeviceKey);
if (vendorId > 0) {
auto vid = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vendorId);
CFDictionaryAddValue(matchingDict, CFSTR(kIOHIDVendorIDKey), vid);
CFRelease(vid);
}
if (productId > 0) {
auto pid = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vendorId);
CFDictionaryAddValue(matchingDict, CFSTR(kIOHIDProductIDKey), pid);
CFRelease(pid);
}
IOHIDManagerSetDeviceMatching(m_mgr, matchingDict);
CFRelease(matchingDict);
} else {
IOHIDManagerSetDeviceMatching(m_mgr, nullptr);
}
QPointer that = this;
if (arrived) {
IOHIDManagerRegisterDeviceMatchingCallback(m_mgr, [](void* ctx, IOReturn, void*, IOHIDDeviceRef device) {
static_cast<DeviceListenerMac*>(ctx)->onDeviceStateChanged(true, device);
}, that);
}
if (left) {
IOHIDManagerRegisterDeviceRemovalCallback(m_mgr, [](void* ctx, IOReturn, void*, IOHIDDeviceRef device) {
static_cast<DeviceListenerMac*>(ctx)->onDeviceStateChanged(true, device);
}, that);
}
if (IOHIDManagerOpen(m_mgr, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
qWarning("Could not open enumerated devices.");
}
}
void DeviceListenerMac::deregisterHotplugCallback()
{
if (m_mgr) {
IOHIDManagerRegisterDeviceMatchingCallback(m_mgr, nullptr, this);
IOHIDManagerRegisterDeviceRemovalCallback(m_mgr, nullptr, this);
}
}
void DeviceListenerMac::onDeviceStateChanged(bool state, void* device)
{
emit devicePlugged(state, m_mgr, device);
}

View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICELISTENER_MAC_H
#define DEVICELISTENER_MAC_H
#define DEVICELISTENER_IMPL DeviceListenerMac
#include <QObject>
#include <IOKit/hid/IOHIDManager.h>
class QUuid;
class DeviceListenerMac : public QObject
{
Q_OBJECT
public:
explicit DeviceListenerMac(QObject* parent);
DeviceListenerMac(const DeviceListenerMac&) = delete;
~DeviceListenerMac() override;
void registerHotplugCallback(bool arrived,
bool left,
int vendorId = -1,
int productId = -1, const QUuid* = nullptr);
void deregisterHotplugCallback();
signals:
void devicePlugged(bool state, void* ctx, void* device);
private:
void onDeviceStateChanged(bool state, void* device);
IOHIDManagerRef m_mgr;
};
#endif // DEVICELISTENER_MAC_H

View File

@ -0,0 +1,124 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "DeviceListenerLibUsb.h"
#include "core/Tools.h"
#include <QPointer>
#include <QtConcurrent>
#include <libusb.h>
DeviceListenerLibUsb::DeviceListenerLibUsb(QWidget* parent)
: QObject(parent)
, m_ctx(nullptr)
, m_completed(false)
{
}
DeviceListenerLibUsb::~DeviceListenerLibUsb()
{
if (m_ctx) {
deregisterAllHotplugCallbacks();
libusb_exit(static_cast<libusb_context*>(m_ctx));
m_ctx = nullptr;
}
}
namespace
{
void handleUsbEvents(libusb_context* ctx, QAtomicInt* completed)
{
while (!*completed) {
libusb_handle_events_completed(ctx, reinterpret_cast<int*>(completed));
Tools::sleep(100);
}
}
} // namespace
int DeviceListenerLibUsb::registerHotplugCallback(bool arrived, bool left, int vendorId, int productId, const QUuid*)
{
if (!m_ctx) {
if (libusb_init(reinterpret_cast<libusb_context**>(&m_ctx)) != LIBUSB_SUCCESS) {
qWarning("Unable to initialize libusb. USB devices may not be detected properly.");
return 0;
}
}
int events = 0;
if (arrived) {
events |= LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED;
}
if (left) {
events |= LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT;
}
int handle = 0;
const QPointer that = this;
const int ret = libusb_hotplug_register_callback(
static_cast<libusb_context*>(m_ctx),
static_cast<libusb_hotplug_event>(events),
static_cast<libusb_hotplug_flag>(0),
vendorId,
productId,
LIBUSB_HOTPLUG_MATCH_ANY,
[](libusb_context* ctx, libusb_device* device, libusb_hotplug_event event, void* userData) -> int {
if (!ctx) {
return 0;
}
emit static_cast<DeviceListenerLibUsb*>(userData)->devicePlugged(
event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED, ctx, device);
return 0;
},
that,
&handle);
if (ret != LIBUSB_SUCCESS) {
qWarning("Failed to register USB listener callback.");
handle = 0;
}
if (m_completed && m_usbEvents.isRunning()) {
// Avoid race conditions
m_usbEvents.waitForFinished();
}
if (!m_usbEvents.isRunning()) {
m_completed = false;
m_usbEvents = QtConcurrent::run(handleUsbEvents, static_cast<libusb_context*>(m_ctx), &m_completed);
}
m_callbackHandles.insert(handle);
return handle;
}
void DeviceListenerLibUsb::deregisterHotplugCallback(int handle)
{
if (!m_ctx || !m_callbackHandles.contains(handle)) {
return;
}
libusb_hotplug_deregister_callback(static_cast<libusb_context*>(m_ctx), handle);
m_callbackHandles.remove(handle);
if (m_callbackHandles.isEmpty() && m_usbEvents.isRunning()) {
m_completed = true;
m_usbEvents.waitForFinished();
}
}
void DeviceListenerLibUsb::deregisterAllHotplugCallbacks()
{
while (!m_callbackHandles.isEmpty()) {
deregisterHotplugCallback(*m_callbackHandles.constBegin());
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICELISTENER_LIBUSB_H
#define DEVICELISTENER_LIBUSB_H
#define DEVICELISTENER_IMPL DeviceListenerLibUsb
#include <QAtomicInt>
#include <QFuture>
#include <QSet>
#include <QWidget>
class QUuid;
class DeviceListenerLibUsb : public QObject
{
Q_OBJECT
public:
explicit DeviceListenerLibUsb(QWidget* parent);
DeviceListenerLibUsb(const DeviceListenerLibUsb&) = delete;
~DeviceListenerLibUsb() override;
int registerHotplugCallback(bool arrived, bool left, int vendorId = -1, int productId = -1, const QUuid* = nullptr);
void deregisterHotplugCallback(int handle);
void deregisterAllHotplugCallbacks();
signals:
void devicePlugged(bool state, void* ctx, void* device);
private:
void* m_ctx;
QSet<int> m_callbackHandles;
QFuture<void> m_usbEvents;
QAtomicInt m_completed;
};
#endif // DEVICELISTENER_LIBUSB_H

View File

@ -17,6 +17,7 @@
#ifndef SCREENLOCKLISTENERDBUS_H #ifndef SCREENLOCKLISTENERDBUS_H
#define SCREENLOCKLISTENERDBUS_H #define SCREENLOCKLISTENERDBUS_H
#include "gui/osutils/ScreenLockListenerPrivate.h" #include "gui/osutils/ScreenLockListenerPrivate.h"
#include <QDBusMessage> #include <QDBusMessage>
@ -24,7 +25,7 @@ class ScreenLockListenerDBus : public ScreenLockListenerPrivate
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ScreenLockListenerDBus(QWidget* parent = nullptr); explicit ScreenLockListenerDBus(QWidget* parent);
private slots: private slots:
void gnomeSessionStatusChanged(uint status); void gnomeSessionStatusChanged(uint status);

View File

@ -0,0 +1,105 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "DeviceListenerWin.h"
#include <QCoreApplication>
#include <windows.h>
#include <winuser.h>
#include <dbt.h>
DeviceListenerWin::DeviceListenerWin(QWidget* parent)
: QObject(parent)
{
// Event listeners need a valid window reference
Q_ASSERT(parent);
QCoreApplication::instance()->installNativeEventFilter(this);
}
DeviceListenerWin::~DeviceListenerWin()
{
deregisterHotplugCallback();
}
void DeviceListenerWin::registerHotplugCallback(bool arrived,
bool left,
int vendorId,
int productId,
const QUuid* deviceClass)
{
Q_ASSERT(deviceClass);
if (m_deviceNotifyHandle) {
deregisterHotplugCallback();
}
QString regex = R"(^\\{2}\?\\[A-Z]+#)";
if (vendorId > 0) {
regex += QString("VID_%1&").arg(vendorId, 0, 16).toUpper();
if (productId > 0) {
regex += QString("PID_%1&").arg(productId, 0, 16).toUpper();
}
}
m_deviceIdMatch = QRegularExpression(regex);
DEV_BROADCAST_DEVICEINTERFACE_W notificationFilter{
sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE, 0u, *deviceClass, {0x00}};
auto w = reinterpret_cast<HWND>(qobject_cast<QWidget*>(parent())->winId());
m_deviceNotifyHandle = RegisterDeviceNotificationW(w, &notificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
if (!m_deviceNotifyHandle) {
qWarning("Failed to register device notification handle.");
return;
}
m_handleArrival = arrived;
m_handleRemoval = left;
}
void DeviceListenerWin::deregisterHotplugCallback()
{
if (m_deviceNotifyHandle) {
UnregisterDeviceNotification(m_deviceNotifyHandle);
m_deviceNotifyHandle = nullptr;
m_handleArrival = false;
m_handleRemoval = false;
}
}
bool DeviceListenerWin::nativeEventFilter(const QByteArray& eventType, void* message, long*)
{
if (eventType != "windows_generic_MSG") {
return false;
}
const auto* m = static_cast<MSG*>(message);
if (m->message != WM_DEVICECHANGE) {
return false;
}
if ((m_handleArrival && m->wParam == DBT_DEVICEARRIVAL)
|| (m_handleRemoval && m->wParam == DBT_DEVICEREMOVECOMPLETE)) {
const auto pBrHdr = reinterpret_cast<PDEV_BROADCAST_HDR>(m->lParam);
const auto pDevIface = reinterpret_cast<PDEV_BROADCAST_DEVICEINTERFACE_W>(pBrHdr);
const auto name = QString::fromWCharArray(pDevIface->dbcc_name, pDevIface->dbcc_size);
if (m_deviceIdMatch.match(name).hasMatch()) {
emit devicePlugged(m->wParam == DBT_DEVICEARRIVAL, nullptr, pDevIface);
return true;
}
}
return false;
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef DEVICELISTENER_WIN_H
#define DEVICELISTENER_WIN_H
#define DEVICELISTENER_IMPL DeviceListenerWin
#include <QAbstractNativeEventFilter>
#include <QRegularExpression>
#include <QUuid>
#include <QWidget>
class DeviceListenerWin : public QObject, public QAbstractNativeEventFilter
{
Q_OBJECT
public:
static constexpr QUuid DEV_CLS_USB =
QUuid(0xa5dcbf10L, 0x6530, 0x11d2, 0x90, 0x1f, 0x00, 0xc0, 0x4f, 0xB9, 0x51, 0xed);
static constexpr QUuid DEV_CLS_KEYBOARD =
QUuid(0x884b96c3L, 0x56ef, 0x11d1, 0xbc, 0x8c, 0x00, 0xa0, 0xc9, 0x14, 0x05, 0xdd);
static constexpr QUuid DEV_CLS_CCID =
QUuid(0x50dd5230L, 0xba8a, 0x11d1, 0xbf, 0x5d, 0x00, 0x00, 0xf8, 0x05, 0xf5, 0x30);
explicit DeviceListenerWin(QWidget* parent);
DeviceListenerWin(const DeviceListenerWin&) = delete;
~DeviceListenerWin() override;
void registerHotplugCallback(bool arrived,
bool left,
int vendorId = -1,
int productId = -1,
const QUuid* deviceClass = nullptr);
void deregisterHotplugCallback();
bool nativeEventFilter(const QByteArray& eventType, void* message, long*) override;
signals:
void devicePlugged(bool state, void* ctx, void* device);
private:
void* m_deviceNotifyHandle = nullptr;
bool m_handleArrival = false;
bool m_handleRemoval = false;
QRegularExpression m_deviceIdMatch;
};
#endif // DEVICELISTENER_WIN_H

View File

@ -17,8 +17,8 @@
#ifndef SCREENLOCKLISTENERWIN_H #ifndef SCREENLOCKLISTENERWIN_H
#define SCREENLOCKLISTENERWIN_H #define SCREENLOCKLISTENERWIN_H
#include <QAbstractNativeEventFilter> #include <QAbstractNativeEventFilter>
#include <QObject>
#include <QWidget> #include <QWidget>
#include "gui/osutils/ScreenLockListenerPrivate.h" #include "gui/osutils/ScreenLockListenerPrivate.h"
@ -27,9 +27,9 @@ class ScreenLockListenerWin : public ScreenLockListenerPrivate, public QAbstract
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ScreenLockListenerWin(QWidget* parent = nullptr); explicit ScreenLockListenerWin(QWidget* parent);
~ScreenLockListenerWin(); ~ScreenLockListenerWin();
bool nativeEventFilter(const QByteArray& eventType, void* message, long*) override; virtual bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
private: private:
void* m_powerNotificationHandle; void* m_powerNotificationHandle;

View File

@ -20,10 +20,13 @@
#include "YubiKeyInterfacePCSC.h" #include "YubiKeyInterfacePCSC.h"
#include "YubiKeyInterfaceUSB.h" #include "YubiKeyInterfaceUSB.h"
#include <QMutexLocker>
#include <QSet>
#include <QtConcurrent> #include <QtConcurrent>
QMutex YubiKey::s_interfaceMutex(QMutex::Recursive);
YubiKey::YubiKey() YubiKey::YubiKey()
: m_interfaces_detect_mutex(QMutex::Recursive)
{ {
int num_interfaces = 0; int num_interfaces = 0;
@ -70,78 +73,39 @@ bool YubiKey::isInitialized()
bool YubiKey::findValidKeys() bool YubiKey::findValidKeys()
{ {
bool found = false; QMutexLocker lock(&s_interfaceMutex);
if (m_interfaces_detect_mutex.tryLock(1000)) {
found |= YubiKeyInterfaceUSB::instance()->findValidKeys(); m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys();
found |= YubiKeyInterfacePCSC::instance()->findValidKeys(); m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys();
m_interfaces_detect_mutex.unlock();
} return !m_usbKeys.isEmpty() || !m_pcscKeys.isEmpty();
return found;
} }
void YubiKey::findValidKeysAsync() void YubiKey::findValidKeysAsync()
{ {
QtConcurrent::run([this] { QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
bool found = findValidKeys();
emit detectComplete(found);
});
} }
QList<YubiKeySlot> YubiKey::foundKeys() YubiKey::KeyMap YubiKey::foundKeys()
{ {
QList<YubiKeySlot> foundKeys; QMutexLocker lock(&s_interfaceMutex);
KeyMap foundKeys;
auto keys = YubiKeyInterfaceUSB::instance()->foundKeys(); for (auto i = m_usbKeys.cbegin(); i != m_usbKeys.cend(); ++i) {
QList<unsigned int> handledSerials = keys.uniqueKeys(); foundKeys.insert(i.key(), i.value());
for (auto serial : handledSerials) {
for (const auto& key : keys.values(serial)) {
foundKeys.append({serial, key.first});
}
} }
keys = YubiKeyInterfacePCSC::instance()->foundKeys(); for (auto i = m_pcscKeys.cbegin(); i != m_pcscKeys.cend(); ++i) {
for (auto serial : keys.uniqueKeys()) { foundKeys.insert(i.key(), i.value());
// Ignore keys that were detected on USB interface already
if (handledSerials.contains(serial)) {
continue;
}
for (const auto& key : keys.values(serial)) {
foundKeys.append({serial, key.first});
}
} }
return foundKeys; return foundKeys;
} }
QString YubiKey::getDisplayName(YubiKeySlot slot)
{
QString name;
name.clear();
if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
name += YubiKeyInterfaceUSB::instance()->getDisplayName(slot);
}
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
// In some cases, the key might present on two interfaces
// This should usually never happen, because the PCSC interface
// filters the "virtual yubikey reader device".
if (!name.isNull()) {
name += " = ";
}
name += YubiKeyInterfacePCSC::instance()->getDisplayName(slot);
}
if (!name.isNull()) {
return name;
}
return tr("%1 No interface, slot %2").arg(QString::number(slot.first), QString::number(slot.second));
}
QString YubiKey::errorMessage() QString YubiKey::errorMessage()
{ {
QMutexLocker lock(&s_interfaceMutex);
QString error; QString error;
error.clear(); error.clear();
if (!m_error.isNull()) { if (!m_error.isNull()) {
@ -177,11 +141,13 @@ QString YubiKey::errorMessage()
*/ */
bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock) bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
{ {
if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) { QMutexLocker lock(&s_interfaceMutex);
if (m_usbKeys.contains(slot)) {
return YubiKeyInterfaceUSB::instance()->testChallenge(slot, wouldBlock); return YubiKeyInterfaceUSB::instance()->testChallenge(slot, wouldBlock);
} }
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) { if (m_pcscKeys.contains(slot)) {
return YubiKeyInterfacePCSC::instance()->testChallenge(slot, wouldBlock); return YubiKeyInterfacePCSC::instance()->testChallenge(slot, wouldBlock);
} }
@ -200,23 +166,25 @@ bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
YubiKey::ChallengeResult YubiKey::ChallengeResult
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
{ {
QMutexLocker lock(&s_interfaceMutex);
m_error.clear(); m_error.clear();
// Make sure we tried to find available keys // Make sure we tried to find available keys
if (foundKeys().isEmpty()) { if (m_usbKeys.isEmpty() && m_pcscKeys.isEmpty()) {
findValidKeys(); findValidKeys();
} }
if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) { if (m_usbKeys.contains(slot)) {
return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response); return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
} }
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) { if (m_pcscKeys.contains(slot)) {
return YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response); return YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
} }
m_error = tr("Could not find interface for hardware key with serial number %1. Please connect it to continue.") m_error = tr("Could not find interface for hardware key with serial number %1. Please connect it to continue.")
.arg(slot.first); .arg(slot.first);
return YubiKey::ChallengeResult::YCR_ERROR; return ChallengeResult::YCR_ERROR;
} }

View File

@ -20,6 +20,7 @@
#define KEEPASSX_YUBIKEY_H #define KEEPASSX_YUBIKEY_H
#include <QHash> #include <QHash>
#include <QMultiMap>
#include <QMutex> #include <QMutex>
#include <QObject> #include <QObject>
#include <QTimer> #include <QTimer>
@ -36,6 +37,7 @@ class YubiKey : public QObject
Q_OBJECT Q_OBJECT
public: public:
typedef QMap<YubiKeySlot, QString> KeyMap;
enum class ChallengeResult : int enum class ChallengeResult : int
{ {
YCR_ERROR = 0, YCR_ERROR = 0,
@ -49,8 +51,7 @@ public:
bool findValidKeys(); bool findValidKeys();
void findValidKeysAsync(); void findValidKeysAsync();
QList<YubiKeySlot> foundKeys(); KeyMap foundKeys();
QString getDisplayName(YubiKeySlot slot);
ChallengeResult challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response); ChallengeResult challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response);
bool testChallenge(YubiKeySlot slot, bool* wouldBlock = nullptr); bool testChallenge(YubiKeySlot slot, bool* wouldBlock = nullptr);
@ -85,7 +86,11 @@ private:
QTimer m_interactionTimer; QTimer m_interactionTimer;
bool m_initialized = false; bool m_initialized = false;
QString m_error; QString m_error;
QMutex m_interfaces_detect_mutex;
static QMutex s_interfaceMutex;
KeyMap m_usbKeys;
KeyMap m_pcscKeys;
Q_DISABLE_COPY(YubiKey) Q_DISABLE_COPY(YubiKey)
}; };

View File

@ -19,10 +19,7 @@
#include "YubiKeyInterface.h" #include "YubiKeyInterface.h"
YubiKeyInterface::YubiKeyInterface() YubiKeyInterface::YubiKeyInterface()
: m_mutex(QMutex::Recursive)
{ {
m_interactionTimer.setSingleShot(true);
m_interactionTimer.setInterval(300);
} }
bool YubiKeyInterface::isInitialized() const bool YubiKeyInterface::isInitialized() const
@ -30,36 +27,6 @@ bool YubiKeyInterface::isInitialized() const
return m_initialized; return m_initialized;
} }
QMultiMap<unsigned int, QPair<int, QString>> YubiKeyInterface::foundKeys()
{
return m_foundKeys;
}
bool YubiKeyInterface::hasFoundKey(YubiKeySlot slot)
{
// A serial number of 0 implies use the first key
if (slot.first == 0 && !m_foundKeys.isEmpty()) {
return true;
}
for (const auto& key : m_foundKeys.values(slot.first)) {
if (slot.second == key.first) {
return true;
}
}
return false;
}
QString YubiKeyInterface::getDisplayName(YubiKeySlot slot)
{
for (const auto& key : m_foundKeys.values(slot.first)) {
if (slot.second == key.first) {
return key.second;
}
}
return tr("%1 Invalid slot specified - %2").arg(QString::number(slot.first), QString::number(slot.second));
}
QString YubiKeyInterface::errorMessage() QString YubiKeyInterface::errorMessage()
{ {
return m_error; return m_error;

View File

@ -20,7 +20,6 @@
#define KEEPASSX_YUBIKEY_INTERFACE_H #define KEEPASSX_YUBIKEY_INTERFACE_H
#include "YubiKey.h" #include "YubiKey.h"
#include <QMultiMap> #include <QMultiMap>
/** /**
@ -32,11 +31,8 @@ class YubiKeyInterface : public QObject
public: public:
bool isInitialized() const; bool isInitialized() const;
QMultiMap<unsigned int, QPair<int, QString>> foundKeys();
bool hasFoundKey(YubiKeySlot slot);
QString getDisplayName(YubiKeySlot slot);
virtual bool findValidKeys() = 0; virtual YubiKey::KeyMap findValidKeys() = 0;
virtual YubiKey::ChallengeResult virtual YubiKey::ChallengeResult
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) = 0; challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) = 0;
virtual bool testChallenge(YubiKeySlot slot, bool* wouldBlock) = 0; virtual bool testChallenge(YubiKeySlot slot, bool* wouldBlock) = 0;
@ -60,7 +56,6 @@ signals:
protected: protected:
explicit YubiKeyInterface(); explicit YubiKeyInterface();
virtual YubiKey::ChallengeResult performChallenge(void* key, virtual YubiKey::ChallengeResult performChallenge(void* key,
int slot, int slot,
bool mayBlock, bool mayBlock,
@ -68,10 +63,6 @@ protected:
Botan::secure_vector<char>& response) = 0; Botan::secure_vector<char>& response) = 0;
virtual bool performTestChallenge(void* key, int slot, bool* wouldBlock) = 0; virtual bool performTestChallenge(void* key, int slot, bool* wouldBlock) = 0;
QMultiMap<unsigned int, QPair<int, QString>> m_foundKeys;
QMutex m_mutex;
QTimer m_interactionTimer;
bool m_initialized = false; bool m_initialized = false;
QString m_error; QString m_error;

View File

@ -466,40 +466,6 @@ namespace
return SCARD_E_NO_SMARTCARD; return SCARD_E_NO_SMARTCARD;
} }
/***
* @brief Reads the status of a key
*
* The status is used for the firmware version only atm.
*
* @param handle Smartcard handle and applet ID bytestring pair
* @param version The firmware version in [major, minor, patch] format
*
* @return SCARD_S_SUCCESS on success
*/
RETVAL getStatus(const SCardAID& handle, uint8_t version[3])
{
// Ensure the transmission is retransmitted after card resets
return transactRetry(handle.first, [&handle, &version]() {
auto rv = selectApplet(handle);
// Ensure that the card is always selected before sending the command
if (rv != SCARD_S_SUCCESS) {
return rv;
}
uint8_t pbSendBuffer[5] = {CLA_ISO, INS_STATUS, 0, 0, 6};
uint8_t pbRecvBuffer[8] = {0}; // 4 bytes serial, 2 bytes other stuff, 2 bytes status
SCUINT dwRecvLength = 8;
rv = transmit(handle.first, pbSendBuffer, 5, pbRecvBuffer, dwRecvLength);
if (rv == SCARD_S_SUCCESS && dwRecvLength >= 3) {
memcpy(version, pbRecvBuffer, 3);
}
return rv;
});
}
/*** /***
* @brief Performs a challenge-response transmission * @brief Performs a challenge-response transmission
* *
@ -575,19 +541,17 @@ YubiKeyInterfacePCSC* YubiKeyInterfacePCSC::instance()
return m_instance; return m_instance;
} }
bool YubiKeyInterfacePCSC::findValidKeys() YubiKey::KeyMap YubiKeyInterfacePCSC::findValidKeys()
{ {
m_error.clear(); m_error.clear();
if (!isInitialized()) { if (!isInitialized()) {
return false; return {};
} }
// Remove all known keys
m_foundKeys.clear(); YubiKey::KeyMap foundKeys;
// Connect to each reader and look for cards // Connect to each reader and look for cards
auto readers_list = getReaders(m_sc_context); for (const auto& reader_name : getReaders(m_sc_context)) {
foreach (const QString& reader_name, readers_list) {
/* Some Yubikeys present their PCSC interface via USB as well /* Some Yubikeys present their PCSC interface via USB as well
Although this would not be a problem in itself, Although this would not be a problem in itself,
we filter these connections because in USB mode, we filter these connections because in USB mode,
@ -608,65 +572,70 @@ bool YubiKeyInterfacePCSC::findValidKeys()
&hCard, &hCard,
&dwActiveProtocol); &dwActiveProtocol);
if (rv == SCARD_S_SUCCESS) { if (rv != SCARD_S_SUCCESS) {
// Read the protocol and the ATR record // Cannot connect to the reader
char pbReader[MAX_READERNAME] = {0}; continue;
SCUINT dwReaderLen = sizeof(pbReader); }
SCUINT dwState = 0;
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
uint8_t pbAtr[MAX_ATR_SIZE] = {0};
SCUINT dwAtrLen = sizeof(pbAtr);
rv = SCardStatus(hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen); // Read the protocol and the ATR record
if (rv == SCARD_S_SUCCESS && (dwProt == SCARD_PROTOCOL_T0 || dwProt == SCARD_PROTOCOL_T1)) { char pbReader[MAX_READERNAME] = {0};
// Find which AID to use SCUINT dwReaderLen = sizeof(pbReader);
SCardAID satr; SCUINT dwState = 0;
if (findAID(hCard, m_aid_codes, satr)) { SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
// Build the UI name using the display name found in the ATR map uint8_t pbAtr[MAX_ATR_SIZE] = {0};
QByteArray atr(reinterpret_cast<char*>(pbAtr), dwAtrLen); SCUINT dwAtrLen = sizeof(pbAtr);
QString name("Unknown Key");
if (m_atr_names.contains(atr)) {
name = m_atr_names.value(atr);
}
// Add the firmware version and the serial number
uint8_t version[3] = {0};
getStatus(satr, version);
name +=
QString(" v%1.%2.%3")
.arg(QString::number(version[0]), QString::number(version[1]), QString::number(version[2]));
unsigned int serial = 0; rv = SCardStatus(hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
getSerial(satr, serial); if (rv != SCARD_S_SUCCESS || (dwProt != SCARD_PROTOCOL_T0 && dwProt != SCARD_PROTOCOL_T1)) {
// Could not read the ATR record or the protocol is not supported
continue;
}
/* This variable indicates that the key is locked / timed out. // Find which AID to use
When using the key via NFC, the user has to re-present the key to clear the timeout. SCardAID satr;
Also, the key can be programmatically reset (see below). if (findAID(hCard, m_aid_codes, satr)) {
When using the key via USB (where the Yubikey presents as a PCSC reader in itself), // Build the UI name using the display name found in the ATR map
the non-HMAC-SHA1 slots (eg. OTP) are incorrectly recognized as locked HMAC-SHA1 slots. QByteArray atr(reinterpret_cast<char*>(pbAtr), dwAtrLen);
Due to this conundrum, we exclude "locked" keys from the key enumeration, QString name("Unknown Key");
but only if the reader is the "virtual yubikey reader device". if (m_atr_names.contains(atr)) {
This also has the nice side effect of de-duplicating interfaces when a key name = m_atr_names.value(atr);
Is connected via USB and also accessible via PCSC */ }
bool wouldBlock = false;
/* When the key is used via NFC, the lock state / time-out is cleared when unsigned int serial = 0;
the smartcard connection is re-established / the applet is selected getSerial(satr, serial);
so the next call to performTestChallenge actually clears the lock.
Due to this the key is unlocked, and we display it as such. /* This variable indicates that the key is locked / timed out.
When the key times out in the time between the key listing and When using the key via NFC, the user has to re-present the key to clear the timeout.
the database unlock /save, an interaction request will be displayed. */ Also, the key can be programmatically reset (see below).
for (int slot = 1; slot <= 2; ++slot) { When using the key via USB (where the Yubikey presents as a PCSC reader in itself),
if (performTestChallenge(&satr, slot, &wouldBlock)) { the non-HMAC-SHA1 slots (eg. OTP) are incorrectly recognized as locked HMAC-SHA1 slots.
auto display = tr("(PCSC) %1 [%2] Challenge-Response - Slot %3") Due to this conundrum, we exclude "locked" keys from the key enumeration,
.arg(name, QString::number(serial), QString::number(slot)); but only if the reader is the "virtual yubikey reader device".
m_foundKeys.insert(serial, {slot, display}); This also has the nice side effect of de-duplicating interfaces when a key
} Is connected via USB and also accessible via PCSC */
} bool wouldBlock = false;
/* When the key is used via NFC, the lock state / time-out is cleared when
the smartcard connection is re-established / the applet is selected
so the next call to performTestChallenge actually clears the lock.
Due to this the key is unlocked, and we display it as such.
When the key times out in the time between the key listing and
the database unlock /save, an interaction request will be displayed. */
for (int slot = 1; slot <= 2; ++slot) {
if (performTestChallenge(&satr, slot, &wouldBlock)) {
auto display =
tr("(NFC) %1 [%2] - Slot %3, %4", "YubiKey display fields")
.arg(name,
QString::number(serial),
QString::number(slot),
wouldBlock ? tr("Press", "USB Challenge-Response Key interaction request")
: tr("Passive", "USB Challenge-Response Key no interaction required"));
foundKeys.insert({serial, slot}, display);
} }
} }
} }
} }
return !m_foundKeys.isEmpty(); return foundKeys;
} }
bool YubiKeyInterfacePCSC::testChallenge(YubiKeySlot slot, bool* wouldBlock) bool YubiKeyInterfacePCSC::testChallenge(YubiKeySlot slot, bool* wouldBlock)
@ -707,12 +676,6 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
return YubiKey::ChallengeResult::YCR_ERROR; return YubiKey::ChallengeResult::YCR_ERROR;
} }
// Try to grab a lock for 1 second, fail out if not possible
if (!m_mutex.tryLock(1000)) {
m_error = tr("Hardware key is currently in use.");
return YubiKey::ChallengeResult::YCR_ERROR;
}
// Try for a few seconds to find the key // Try for a few seconds to find the key
emit challengeStarted(); emit challengeStarted();
@ -732,7 +695,6 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
resets the key (see comment above) */ resets the key (see comment above) */
if (ret == YubiKey::ChallengeResult::YCR_SUCCESS) { if (ret == YubiKey::ChallengeResult::YCR_SUCCESS) {
emit challengeCompleted(); emit challengeCompleted();
m_mutex.unlock();
return ret; return ret;
} }
} }
@ -746,7 +708,6 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
.arg(slot.first) .arg(slot.first)
+ m_error; + m_error;
emit challengeCompleted(); emit challengeCompleted();
m_mutex.unlock();
return YubiKey::ChallengeResult::YCR_ERROR; return YubiKey::ChallengeResult::YCR_ERROR;
} }

View File

@ -52,7 +52,7 @@ class YubiKeyInterfacePCSC : public YubiKeyInterface
public: public:
static YubiKeyInterfacePCSC* instance(); static YubiKeyInterfacePCSC* instance();
bool findValidKeys() override; YubiKey::KeyMap findValidKeys() override;
YubiKey::ChallengeResult YubiKey::ChallengeResult
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override; challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override;
@ -71,7 +71,7 @@ private:
Botan::secure_vector<char>& response) override; Botan::secure_vector<char>& response) override;
bool performTestChallenge(void* key, int slot, bool* wouldBlock) override; bool performTestChallenge(void* key, int slot, bool* wouldBlock) override;
SCARDCONTEXT m_sc_context; SCARDCONTEXT m_sc_context{};
// This list contains all the AID (application identifier) codes for the Yubikey HMAC-SHA1 applet // This list contains all the AID (application identifier) codes for the Yubikey HMAC-SHA1 applet
// and also for compatible third-party ones. They will be tried one by one. // and also for compatible third-party ones. They will be tried one by one.
@ -86,16 +86,13 @@ private:
const QHash<QByteArray, QString> m_atr_names = { const QHash<QByteArray, QString> m_atr_names = {
// Yubico Yubikeys // Yubico Yubikeys
{QByteArrayLiteral("\x3B\x8C\x80\x01\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\x33\x58"), "YubiKey NEO"}, {QByteArrayLiteral("\x3B\x8C\x80\x01\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\x33\x58"), "YubiKey NEO"},
{QByteArrayLiteral("\x3B\x8C\x80\x01\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\xFF\x94"), {QByteArrayLiteral("\x3B\x8C\x80\x01\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\xFF\x94"), "YubiKey NEO"},
"YubiKey NEO via NFC"}, {QByteArrayLiteral("\x3B\x8D\x80\x01\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\x79\xF9"), "YubiKey 5"},
{QByteArrayLiteral("\x3B\x8D\x80\x01\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\x79\xF9"), {QByteArrayLiteral("\x3B\x8D\x80\x01\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\xFF\x7F"), "YubiKey 5"},
"YubiKey 5 NFC via NFC"},
{QByteArrayLiteral("\x3B\x8D\x80\x01\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\xFF\x7F"),
"YubiKey 5 NFC via ACR122U"},
{QByteArrayLiteral("\x3B\xF8\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x34\xD4"), {QByteArrayLiteral("\x3B\xF8\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x34\xD4"),
"YubiKey 4 OTP+CCID"}, "YubiKey 4 - OTP+CCID"},
{QByteArrayLiteral("\x3B\xF9\x18\x00\xFF\x81\x31\xFE\x45\x50\x56\x5F\x4A\x33\x41\x30\x34\x30\x40"), {QByteArrayLiteral("\x3B\xF9\x18\x00\xFF\x81\x31\xFE\x45\x50\x56\x5F\x4A\x33\x41\x30\x34\x30\x40"),
"YubiKey NEO OTP+U2F+CCID (PKI)"}, "YubiKey NEO - OTP+U2F+CCID (PKI)"},
{QByteArrayLiteral("\x3B\xFA\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\xA6"), {QByteArrayLiteral("\x3B\xFA\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\xA6"),
"YubiKey NEO"}, "YubiKey NEO"},
{QByteArrayLiteral("\x3B\xFC\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\x33\xE1"), {QByteArrayLiteral("\x3B\xFC\x13\x00\x00\x81\x31\xFE\x15\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\x33\xE1"),
@ -104,7 +101,7 @@ private:
"YubiKey NEO"}, "YubiKey NEO"},
{QByteArrayLiteral( {QByteArrayLiteral(
"\x3B\xFD\x13\x00\x00\x81\x31\xFE\x15\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\x79\x40"), "\x3B\xFD\x13\x00\x00\x81\x31\xFE\x15\x80\x73\xC0\x21\xC0\x57\x59\x75\x62\x69\x4B\x65\x79\x40"),
"YubiKey 5 NFC (PKI)"}, "YubiKey 5 (PKI)"},
{QByteArrayLiteral( {QByteArrayLiteral(
"\x3B\xFD\x13\x00\x00\x81\x31\xFE\x45\x41\x37\x30\x30\x36\x43\x47\x20\x32\x34\x32\x52\x31\xD6"), "\x3B\xFD\x13\x00\x00\x81\x31\xFE\x45\x41\x37\x30\x30\x36\x43\x47\x20\x32\x34\x32\x52\x31\xD6"),
"YubiKey NEO (token)"}, "YubiKey NEO (token)"},

View File

@ -21,7 +21,6 @@
#include "core/Tools.h" #include "core/Tools.h"
#include "crypto/Random.h" #include "crypto/Random.h"
#include "thirdparty/ykcore/ykcore.h" #include "thirdparty/ykcore/ykcore.h"
#include "thirdparty/ykcore/ykdef.h"
#include "thirdparty/ykcore/ykstatus.h" #include "thirdparty/ykcore/ykstatus.h"
namespace namespace
@ -82,7 +81,6 @@ namespace
} // namespace } // namespace
YubiKeyInterfaceUSB::YubiKeyInterfaceUSB() YubiKeyInterfaceUSB::YubiKeyInterfaceUSB()
: YubiKeyInterface()
{ {
if (!yk_init()) { if (!yk_init()) {
qDebug("YubiKey: Failed to initialize USB interface."); qDebug("YubiKey: Failed to initialize USB interface.");
@ -107,15 +105,14 @@ YubiKeyInterfaceUSB* YubiKeyInterfaceUSB::instance()
return m_instance; return m_instance;
} }
bool YubiKeyInterfaceUSB::findValidKeys() YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
{ {
m_error.clear(); m_error.clear();
if (!isInitialized()) { if (!isInitialized()) {
return false; return {};
} }
// Remove all known keys YubiKey::KeyMap keyMap;
m_foundKeys.clear();
// Try to detect up to 4 connected hardware keys // Try to detect up to 4 connected hardware keys
for (int i = 0; i < MAX_KEYS; ++i) { for (int i = 0; i < MAX_KEYS; ++i) {
@ -133,13 +130,12 @@ bool YubiKeyInterfaceUSB::findValidKeys()
yk_get_key_vid_pid(yk_key, &vid, &pid); yk_get_key_vid_pid(yk_key, &vid, &pid);
QString name = m_pid_names.value(pid, tr("Unknown")); QString name = m_pid_names.value(pid, tr("Unknown"));
if (vid == 0x1d50) { if (vid == ONLYKEY_VID) {
name = QStringLiteral("OnlyKey"); name = QStringLiteral("OnlyKey %ver");
}
if (name.contains("%ver")) {
name = name.replace("%ver", QString::number(ykds_version_major(st)));
} }
name += QString(" v%1.%2.%3")
.arg(QString::number(ykds_version_major(st)),
QString::number(ykds_version_minor(st)),
QString::number(ykds_version_build(st)));
bool wouldBlock; bool wouldBlock;
for (int slot = 1; slot <= 2; ++slot) { for (int slot = 1; slot <= 2; ++slot) {
@ -151,25 +147,23 @@ bool YubiKeyInterfaceUSB::findValidKeys()
// Don't actually challenge a YubiKey Neo or below, they always require button press // Don't actually challenge a YubiKey Neo or below, they always require button press
// if it is enabled for the slot resulting in failed detection // if it is enabled for the slot resulting in failed detection
if (pid <= NEO_OTP_U2F_CCID_PID) { if (pid <= NEO_OTP_U2F_CCID_PID) {
auto display = tr("(USB) %1 [%2] Configured Slot - %3") auto display = tr("%1 [%2] - Slot %3", "YubiKey NEO display fields")
.arg(name, QString::number(serial), QString::number(slot)); .arg(name, QString::number(serial), QString::number(slot));
m_foundKeys.insert(serial, {slot, display}); keyMap.insert({serial, slot}, display);
} else if (performTestChallenge(yk_key, slot, &wouldBlock)) { } else if (performTestChallenge(yk_key, slot, &wouldBlock)) {
auto display = auto display =
tr("(USB) %1 [%2] Challenge-Response - Slot %3 - %4") tr("%1 [%2] - Slot %3, %4", "YubiKey display fields")
.arg(name, .arg(name,
QString::number(serial), QString::number(serial),
QString::number(slot), QString::number(slot),
wouldBlock ? tr("Press", "USB Challenge-Response Key interaction request") wouldBlock ? tr("Press", "USB Challenge-Response Key interaction request")
: tr("Passive", "USB Challenge-Response Key no interaction required")); : tr("Passive", "USB Challenge-Response Key no interaction required"));
m_foundKeys.insert(serial, {slot, display}); keyMap.insert({serial, slot}, display);
} }
} }
ykds_free(st); ykds_free(st);
closeKey(yk_key); closeKey(yk_key);
Tools::wait(100);
} else if (yk_errno == YK_ENOKEY) { } else if (yk_errno == YK_ENOKEY) {
// No more keys are connected // No more keys are connected
break; break;
@ -180,7 +174,7 @@ bool YubiKeyInterfaceUSB::findValidKeys()
} }
} }
return !m_foundKeys.isEmpty(); return keyMap;
} }
/** /**
@ -198,6 +192,7 @@ bool YubiKeyInterfaceUSB::testChallenge(YubiKeySlot slot, bool* wouldBlock)
if (yk_key) { if (yk_key) {
ret = performTestChallenge(yk_key, slot.second, wouldBlock); ret = performTestChallenge(yk_key, slot.second, wouldBlock);
} }
closeKey(yk_key);
return ret; return ret;
} }
@ -233,18 +228,11 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
return YubiKey::ChallengeResult::YCR_ERROR; return YubiKey::ChallengeResult::YCR_ERROR;
} }
// Try to grab a lock for 1 second, fail out if not possible
if (!m_mutex.tryLock(1000)) {
m_error = tr("Hardware key is currently in use.");
return YubiKey::ChallengeResult::YCR_ERROR;
}
auto* yk_key = openKeySerial(slot.first); auto* yk_key = openKeySerial(slot.first);
if (!yk_key) { if (!yk_key) {
// Key with specified serial number is not connected // Key with specified serial number is not connected
m_error = m_error =
tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first); tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first);
m_mutex.unlock();
return YubiKey::ChallengeResult::YCR_ERROR; return YubiKey::ChallengeResult::YCR_ERROR;
} }
@ -253,7 +241,6 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
closeKey(yk_key); closeKey(yk_key);
emit challengeCompleted(); emit challengeCompleted();
m_mutex.unlock();
return ret; return ret;
} }

View File

@ -32,8 +32,10 @@ class YubiKeyInterfaceUSB : public YubiKeyInterface
public: public:
static YubiKeyInterfaceUSB* instance(); static YubiKeyInterfaceUSB* instance();
static constexpr int YUBICO_USB_VID = YUBICO_VID;
static constexpr int ONLYKEY_USB_VID = ONLYKEY_VID;
bool findValidKeys() override; YubiKey::KeyMap findValidKeys() override;
YubiKey::ChallengeResult YubiKey::ChallengeResult
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override; challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override;
@ -53,22 +55,22 @@ private:
bool performTestChallenge(void* key, int slot, bool* wouldBlock) override; bool performTestChallenge(void* key, int slot, bool* wouldBlock) override;
// This map provides display names for the various USB PIDs of the Yubikeys // This map provides display names for the various USB PIDs of the Yubikeys
const QHash<int, QString> m_pid_names = {{YUBIKEY_PID, "YubiKey 1/2"}, const QHash<int, QString> m_pid_names = {{YUBIKEY_PID, "YubiKey %ver"},
{NEO_OTP_PID, "YubiKey NEO - OTP only"}, {NEO_OTP_PID, "YubiKey NEO - OTP"},
{NEO_OTP_CCID_PID, "YubiKey NEO - OTP and CCID"}, {NEO_OTP_CCID_PID, "YubiKey NEO - OTP+CCID"},
{NEO_CCID_PID, "YubiKey NEO - CCID only"}, {NEO_CCID_PID, "YubiKey NEO - CCID"},
{NEO_U2F_PID, "YubiKey NEO - U2F only"}, {NEO_U2F_PID, "YubiKey NEO - FIDO"},
{NEO_OTP_U2F_PID, "YubiKey NEO - OTP and U2F"}, {NEO_OTP_U2F_PID, "YubiKey NEO - OTP+FIDO"},
{NEO_U2F_CCID_PID, "YubiKey NEO - U2F and CCID"}, {NEO_U2F_CCID_PID, "YubiKey NEO - FIDO+CCID"},
{NEO_OTP_U2F_CCID_PID, "YubiKey NEO - OTP, U2F and CCID"}, {NEO_OTP_U2F_CCID_PID, "YubiKey NEO - OTP+FIDO+CCID"},
{YK4_OTP_PID, "YubiKey 4/5 - OTP only"}, {YK4_OTP_PID, "YubiKey %ver - OTP"},
{YK4_U2F_PID, "YubiKey 4/5 - U2F only"}, {YK4_U2F_PID, "YubiKey %ver - U2F"},
{YK4_OTP_U2F_PID, "YubiKey 4/5 - OTP and U2F"}, {YK4_OTP_U2F_PID, "YubiKey %ver - OTP+FIDO"},
{YK4_CCID_PID, "YubiKey 4/5 - CCID only"}, {YK4_CCID_PID, "YubiKey %ver - CCID"},
{YK4_OTP_CCID_PID, "YubiKey 4/5 - OTP and CCID"}, {YK4_OTP_CCID_PID, "YubiKey %ver - OTP+CCID"},
{YK4_U2F_CCID_PID, "YubiKey 4/5 - U2F and CCID"}, {YK4_U2F_CCID_PID, "YubiKey %ver - FIDO+CCID"},
{YK4_OTP_U2F_CCID_PID, "YubiKey 4/5 - OTP, U2F and CCID"}, {YK4_OTP_U2F_CCID_PID, "YubiKey %ver - OTP+FIDO+CCID"},
{PLUS_U2F_OTP_PID, "YubiKey plus - OTP+U2F"}}; {PLUS_U2F_OTP_PID, "YubiKey plus - OTP+FIDO"}};
}; };
#endif // KEEPASSX_YUBIKEY_INTERFACE_USB_H #endif // KEEPASSX_YUBIKEY_INTERFACE_USB_H

View File

@ -45,17 +45,11 @@ void YubiKey::findValidKeysAsync()
{ {
} }
QList<YubiKeySlot> YubiKey::foundKeys() YubiKey::KeyMap YubiKey::foundKeys()
{ {
return {}; return {};
} }
QString YubiKey::getDisplayName(YubiKeySlot slot)
{
Q_UNUSED(slot);
return {};
}
QString YubiKey::errorMessage() QString YubiKey::errorMessage()
{ {
return {}; return {};

View File

@ -18,7 +18,7 @@
#ifndef KEEPASSXC_WINDOWSHELLO_H #ifndef KEEPASSXC_WINDOWSHELLO_H
#define KEEPASSXC_WINDOWSHELLO_H #define KEEPASSXC_WINDOWSHELLO_H
#include "QuickUnlockInterface.h"; #include "QuickUnlockInterface.h"
#include <QHash> #include <QHash>
#include <QObject> #include <QObject>

View File

@ -2253,7 +2253,7 @@ void TestCli::testYubiKeyOption()
YubiKey::instance()->findValidKeys(); YubiKey::instance()->findValidKeys();
auto keys = YubiKey::instance()->foundKeys(); const auto keys = YubiKey::instance()->foundKeys().keys();
if (keys.isEmpty()) { if (keys.isEmpty()) {
QSKIP("No YubiKey devices were detected."); QSKIP("No YubiKey devices were detected.");
} }

View File

@ -44,11 +44,12 @@ void TestYubiKeyChallengeResponse::testDetectDevices()
YubiKey::instance()->findValidKeys(); YubiKey::instance()->findValidKeys();
// Look at the information retrieved from the key(s) // Look at the information retrieved from the key(s)
for (auto key : YubiKey::instance()->foundKeys()) { const auto foundKeys = YubiKey::instance()->foundKeys();
auto displayName = YubiKey::instance()->getDisplayName(key); for (auto i = foundKeys.cbegin(); i != foundKeys.cend(); ++i) {
const auto& displayName = i.value();
QVERIFY(displayName.contains("Challenge-Response - Slot") || displayName.contains("Configured Slot -")); QVERIFY(displayName.contains("Challenge-Response - Slot") || displayName.contains("Configured Slot -"));
QVERIFY(displayName.contains(QString::number(key.first))); QVERIFY(displayName.contains(QString::number(i.key().first)));
QVERIFY(displayName.contains(QString::number(key.second))); QVERIFY(displayName.contains(QString::number(i.key().second)));
} }
} }
@ -59,7 +60,7 @@ void TestYubiKeyChallengeResponse::testDetectDevices()
*/ */
void TestYubiKeyChallengeResponse::testKeyChallenge() void TestYubiKeyChallengeResponse::testKeyChallenge()
{ {
auto keys = YubiKey::instance()->foundKeys(); auto keys = YubiKey::instance()->foundKeys().keys();
if (keys.isEmpty()) { if (keys.isEmpty()) {
QSKIP("No YubiKey devices were detected."); QSKIP("No YubiKey devices were detected.");
} }

View File

@ -141,6 +141,7 @@ void TestGui::init()
databaseOpenWidget->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit"); databaseOpenWidget->findChild<PasswordWidget*>("editPassword")->findChild<QLineEdit*>("passwordEdit");
QVERIFY(editPassword); QVERIFY(editPassword);
editPassword->setFocus(); editPassword->setFocus();
QTRY_VERIFY(editPassword->hasFocus());
QTest::keyClicks(editPassword, "a"); QTest::keyClicks(editPassword, "a");
QTest::keyClick(editPassword, Qt::Key_Enter); QTest::keyClick(editPassword, Qt::Key_Enter);