Automatically detect USB device changes

This commit is contained in:
Jonathan White 2024-03-09 08:26:42 -05:00
parent 28d096a89a
commit f20b531430
40 changed files with 1504 additions and 824 deletions

View File

@ -570,6 +570,12 @@ include_directories(SYSTEM ${ZLIB_INCLUDE_DIR})
if(WITH_XC_YUBIKEY)
find_package(PCSC REQUIRED)
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()
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/view-history.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/keepassxc.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/view-history.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/internet-web-browser.svg</file>
<file>application/scalable/apps/keepassxc.svg</file>

View File

@ -1530,30 +1530,10 @@ Backup database located at %2</source>
<source>Password field</source>
<translation type="unfinished"></translation>
</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>Key file help</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key slot selection</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware Key:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key help</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Key file to unlock the database</source>
<translation type="unfinished"></translation>
@ -1566,14 +1546,6 @@ Backup database located at %2</source>
<source>Browse</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh hardware tokens</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unlock Database</source>
<translation type="unfinished"></translation>
@ -1663,44 +1635,66 @@ To prevent this error from appearing, you must go to &quot;Database Settings / S
<source>Cannot use database file as key file</source>
<translation type="unfinished"></translation>
</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>
<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>&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>authenticate to access the database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to authenticate with Windows Hello: %1</source>
<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>
<message>
<source>Windows Hello setup was canceled or failed. Quick unlock has not been enabled.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to authenticate with Windows Hello: %1</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@ -9414,10 +9408,6 @@ Example: JBSWY3DPEHPK3PXP</source>
</context>
<context>
<name>YubiKey</name>
<message>
<source>%1 No interface, slot %2</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>General: </source>
<translation type="unfinished"></translation>
@ -9429,14 +9419,6 @@ Example: JBSWY3DPEHPK3PXP</source>
</context>
<context>
<name>YubiKeyEditWidget</name>
<message>
<source>Refresh hardware tokens</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key slot selection</source>
<translation type="unfinished"></translation>
@ -9481,28 +9463,17 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>No hardware keys detected</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>YubiKeyInterface</name>
<message>
<source>%1 Invalid slot specified - %2</source>
<source>Refresh hardware keys</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>YubiKeyInterfacePCSC</name>
<message>
<source>(PCSC) %1 [%2] Challenge-Response - Slot %3</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The YubiKey PCSC interface has not been initialized.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key is currently in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not find or access hardware key with serial number %1. Please present it to continue. </source>
<translation type="unfinished"></translation>
@ -9519,6 +9490,21 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Failed to complete a challenge-response, the PCSC error code was: %1</source>
<translation type="unfinished"></translation>
</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>
<name>YubiKeyInterfaceUSB</name>
@ -9526,14 +9512,6 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Unknown</source>
<translation type="unfinished"></translation>
</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>
<source>Press</source>
<comment>USB Challenge-Response Key interaction request</comment>
@ -9548,10 +9526,6 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>The YubiKey USB interface has not been initialized.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key is currently in use.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not find hardware key with serial number %1. Please plug it in to continue.</source>
<translation type="unfinished"></translation>
@ -9568,5 +9542,15 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>Failed to complete a challenge-response, the specific error was: %1</source>
<translation type="unfinished"></translation>
</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>
</TS>

View File

@ -228,6 +228,17 @@ if(WIN32)
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}
../share/icons/icons.qrc
../share/wizard/wizard.qrc)
@ -376,7 +387,7 @@ if(HAIKU)
target_link_libraries(keepassx_core network)
endif()
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)
target_link_libraries(keepassx_core Qt5::X11Extras X11)
endif()

View File

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

View File

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

View File

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

View File

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

View File

@ -19,13 +19,15 @@
#include "DatabaseOpenWidget.h"
#include "ui_DatabaseOpenWidget.h"
#include "config-keepassx.h"
#include "gui/FileDialog.h"
#include "gui/Icons.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "keys/ChallengeResponseKey.h"
#include "keys/FileKey.h"
#ifdef WITH_XC_YUBIKEY
#include "keys/drivers/YubiKeyInterfaceUSB.h"
#endif
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
@ -73,6 +75,9 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
: DialogyWidget(parent)
, m_ui(new Ui::DatabaseOpenWidget())
, m_db(nullptr)
#ifdef WITH_XC_YUBIKEY
, m_deviceListener(new DeviceListener(this))
#endif
{
m_ui->setupUi(this);
@ -105,18 +110,27 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
m_ui->hardwareKeyLabelHelp->setIcon(icons()->icon("system-help").pixmap(QSize(12, 12)));
connect(m_ui->hardwareKeyLabelHelp, SIGNAL(clicked(bool)), SLOT(openHardwareKeyHelp()));
m_ui->keyFileLabelHelp->setIcon(icons()->icon("system-help").pixmap(QSize(12, 12)));
connect(m_ui->keyFileLabelHelp, SIGNAL(clicked(bool)), SLOT(openKeyFileHelp()));
connect(m_ui->addKeyFileLinkLabel, &QLabel::linkActivated, this, [&](const QString&) {
if (browseKeyFile()) {
toggleKeyFileComponent(true);
}
});
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();
sp.setRetainSizeWhenHidden(true);
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(), &YubiKey::userInteractionRequest, this, [this] {
@ -128,12 +142,17 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
}
});
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
m_ui->hardwareKeyLabel->setVisible(false);
m_ui->hardwareKeyLabelHelp->setVisible(false);
m_ui->buttonRedetectYubikey->setVisible(false);
m_ui->challengeResponseCombo->setVisible(false);
m_ui->hardwareKeyProgress->setVisible(false);
m_ui->noHardwareKeysFoundLabel->setVisible(false);
m_ui->refreshHardwareKeys->setVisible(false);
#endif
// QuickUnlock actions
@ -146,6 +165,32 @@ DatabaseOpenWidget::~DatabaseOpenWidget()
{
}
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)
{
DialogyWidget::showEvent(event);
@ -158,6 +203,24 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event)
m_ui->editPassword->setFocus();
}
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)
@ -168,6 +231,10 @@ void DatabaseOpenWidget::hideEvent(QHideEvent* event)
if (!isVisible()) {
m_hideTimer.start();
}
#ifdef WITH_XC_YUBIKEY
m_deviceListener->deregisterAllHotplugCallbacks();
#endif
}
bool DatabaseOpenWidget::unlockingDatabase()
@ -186,6 +253,7 @@ void DatabaseOpenWidget::load(const QString& filename)
auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
if (lastKeyFiles.contains(m_filename)) {
m_ui->keyFileLineEdit->setText(lastKeyFiles[m_filename].toString());
toggleKeyFileComponent(true);
}
}
@ -197,13 +265,8 @@ void DatabaseOpenWidget::load(const QString& filename)
}
#ifdef WITH_XC_YUBIKEY
// Only auto-poll for hardware keys if we previously used one with this database file
if (config()->get(Config::RememberLastKeyFiles).toBool()) {
auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
if (lastChallengeResponse.contains(m_filename)) {
// Do initial auto-poll
pollHardwareKey();
}
}
#endif
}
@ -215,7 +278,7 @@ void DatabaseOpenWidget::clearForms()
m_ui->keyFileLineEdit->clear();
m_ui->keyFileLineEdit->setShowPassword(false);
m_ui->keyFileLineEdit->setClearButtonEnabled(true);
m_ui->challengeResponseCombo->clear();
m_ui->hardwareKeyCombo->clear();
m_ui->centralStack->setCurrentIndex(0);
m_db.reset();
}
@ -411,9 +474,9 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
lastChallengeResponse.remove(m_filename);
int selectionIndex = m_ui->challengeResponseCombo->currentIndex();
if (selectionIndex > 0) {
auto slot = m_ui->challengeResponseCombo->itemData(selectionIndex).value<YubiKeySlot>();
int selectionIndex = m_ui->hardwareKeyCombo->currentIndex();
if (m_ui->useHardwareKeyCheckBox->isChecked()) {
auto slot = m_ui->hardwareKeyCombo->itemData(selectionIndex).value<YubiKeySlot>();
auto crKey = QSharedPointer<ChallengeResponseKey>(new ChallengeResponseKey(slot));
databaseKey->addChallengeResponseKey(crKey);
@ -434,55 +497,65 @@ void DatabaseOpenWidget::reject()
emit dialogFinished(false);
}
void DatabaseOpenWidget::browseKeyFile()
bool DatabaseOpenWidget::browseKeyFile()
{
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()) {
MessageBox::warning(this,
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, "
"please leave the field empty."),
tr("Your database file is NOT a key file!\nIf you don't have a key file or don't know what "
"that is, you don't have to select one."),
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);
}
return true;
}
void DatabaseOpenWidget::pollHardwareKey()
void DatabaseOpenWidget::pollHardwareKey(bool manualTrigger)
{
if (m_pollingHardwareKey) {
return;
}
m_ui->challengeResponseCombo->clear();
m_ui->challengeResponseCombo->addItem(tr("Detecting hardware keys…"));
m_ui->buttonRedetectYubikey->setEnabled(false);
m_ui->challengeResponseCombo->setEnabled(false);
m_ui->hardwareKeyCombo->setEnabled(false);
m_ui->hardwareKeyProgress->setVisible(true);
m_ui->refreshHardwareKeys->setEnabled(false);
m_ui->noHardwareKeysFoundLabel->setVisible(false);
m_pollingHardwareKey = true;
m_manualHardwareKeyRefresh = manualTrigger;
YubiKey::instance()->findValidKeysAsync();
}
void DatabaseOpenWidget::hardwareKeyResponse(bool found)
{
m_ui->challengeResponseCombo->clear();
m_ui->buttonRedetectYubikey->setEnabled(true);
m_ui->hardwareKeyProgress->setVisible(false);
m_ui->refreshHardwareKeys->setEnabled(true);
m_ui->hardwareKeyCombo->clear();
m_pollingHardwareKey = false;
if (!found) {
m_ui->challengeResponseCombo->addItem(tr("No hardware keys detected"));
m_ui->challengeResponseCombo->setEnabled(false);
toggleHardwareKeyComponent(false);
return;
} else {
m_ui->challengeResponseCombo->addItem(tr("Select hardware key…"));
}
YubiKeySlot lastUsedSlot;
@ -494,31 +567,24 @@ void DatabaseOpenWidget::hardwareKeyResponse(bool found)
if (split.size() > 1) {
lastUsedSlot = YubiKeySlot(split[0].toUInt(), split[1].toInt());
}
m_ui->useHardwareKeyCheckBox->setChecked(true);
}
}
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
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
if (lastUsedSlot == slot) {
selectedIndex = m_ui->challengeResponseCombo->count() - 1;
if (lastUsedSlot == i.key()) {
selectedIndex = m_ui->hardwareKeyCombo->count() - 1;
}
}
m_ui->challengeResponseCombo->setCurrentIndex(selectedIndex);
m_ui->challengeResponseCombo->setEnabled(true);
}
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"));
toggleHardwareKeyComponent(true);
m_ui->hardwareKeyCombo->setEnabled(m_ui->useHardwareKeyCheckBox->isChecked());
m_ui->hardwareKeyCombo->setCurrentIndex(selectedIndex);
}
void DatabaseOpenWidget::setUserInteractionLock(bool state)

View File

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

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>520</width>
<height>436</height>
<width>745</width>
<height>544</height>
</rect>
</property>
<property name="accessibleName">
@ -18,7 +18,7 @@
<widget class="MessageWidget" name="messageWidget" native="true"/>
</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">
<number>0</number>
</property>
@ -40,18 +40,6 @@
</item>
<item>
<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">
<item>
<spacer name="verticalSpacer_2">
@ -71,7 +59,6 @@
<property name="font">
<font>
<pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@ -107,8 +94,8 @@
<widget class="QStackedWidget" name="centralStack">
<property name="minimumSize">
<size>
<width>0</width>
<height>250</height>
<width>650</width>
<height>0</height>
</size>
</property>
<property name="frameShape">
@ -122,28 +109,54 @@
</property>
<widget class="QWidget" name="mainPage">
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>20</number>
<number>30</number>
</property>
<property name="topMargin">
<number>15</number>
<number>25</number>
</property>
<property name="rightMargin">
<number>20</number>
<number>30</number>
</property>
<property name="bottomMargin">
<number>15</number>
<number>25</number>
</property>
<item>
<widget class="QLabel" name="label">
<widget class="QFrame" name="enterPasswordComponent">
<property name="lineWidth">
<number>0</number>
</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>
<property name="buddy">
<cstring>editPassword</cstring>
</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">
@ -155,113 +168,11 @@
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Enter Additional Credentials (if any):</string>
</property>
</widget>
</item>
<item>
<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">
<number>5</number>
</property>
<item>
<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>
</property>
<item row="1" column="2">
<widget class="QProgressBar" name="hardwareKeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
<height>4</height>
</size>
</property>
<property name="minimum">
@ -278,108 +189,48 @@
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="challengeResponseCombo">
<property name="enabled">
<bool>false</bool>
</property>
<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>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="spacing">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="hardwareKeyLabel">
<property name="text">
<string>Hardware Key:</string>
</property>
<property name="buddy">
<cstring>challengeResponseCombo</cstring>
</property>
</layout>
</widget>
</item>
<item>
<widget class="QToolButton" name="hardwareKeyLabelHelp">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
<widget class="QFrame" name="selectKeyFileComponent">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="toolTip">
<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;
&lt;p&gt;Click for more information…&lt;/p&gt;</string>
</property>
<property name="accessibleName">
<string>Hardware key 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>
<spacer name="verticalSpacer_6">
<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 row="0" column="3">
<layout class="QGridLayout" name="gridLayout_2">
<property name="verticalSpacing">
<property name="lineWidth">
<number>0</number>
</property>
<item row="0" column="1">
<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">
@ -395,9 +246,7 @@
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="4">
<item>
<widget class="QPushButton" name="buttonBrowseFile">
<property name="toolTip">
<string>Browse for key file</string>
@ -410,53 +259,187 @@
</property>
</widget>
</item>
<item row="1" column="4">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="addAdditionalKeysComponent">
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="addAdditionalKeysComponentLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</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="QPushButton" name="buttonRedetectYubikey">
<property name="enabled">
<bool>true</bool>
<widget class="QFrame" name="hardwareKeyComponent">
<layout class="QHBoxLayout" name="hardwareKeyComponentLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="toolTip">
<string>Refresh hardware tokens</string>
<property name="leftMargin">
<number>0</number>
</property>
<property name="accessibleName">
<string>Refresh hardware tokens</string>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="useHardwareKeyCheckBox">
<property name="text">
<string>Refresh</string>
<string notr="true">Use Hardware Security Key [Serial: 11111111]</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
<widget class="QComboBox" name="hardwareKeyCombo">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>16777215</height>
</size>
</property>
<property name="accessibleName">
<string>Hardware key slot selection</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="noHardwareKeysFoundLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>No hardware keys found.</string>
</property>
<property name="margin">
<number>1</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>2</height>
<width>40</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
<item>
<widget class="QPushButton" name="refreshHardwareKeys">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="toolTip">
<string>Refresh Hardware Keys</string>
</property>
<property name="accessibleName">
<string>Refresh Hardware Keys</string>
</property>
<property name="styleSheet">
<string notr="true">QPushButton { background-color: transparent; border: none; } </string>
</property>
<property name="text">
<string notr="true"/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="addKeyFileLinkLabel">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="focusPolicy">
<enum>Qt::TabFocus</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;/p&gt;</string>
</property>
<property name="accessibleDescription">
<string>Click to add a key file.</string>
</property>
<property name="text">
<string>&lt;a href=&quot;#&quot; style=&quot;text-decoration: underline&quot;&gt;I have a key file&lt;/a&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="margin">
<number>1</number>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="dialogButtonsLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin">
<number>15</number>
<number>25</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item alignment="Qt::AlignRight">
<widget class="QDialogButtonBox" name="buttonBox">
@ -474,17 +457,20 @@
</widget>
<widget class="QWidget" name="quickUnlockPage">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>20</number>
<number>10</number>
</property>
<property name="topMargin">
<number>15</number>
<number>10</number>
</property>
<property name="rightMargin">
<number>20</number>
<number>10</number>
</property>
<property name="bottomMargin">
<number>15</number>
<number>10</number>
</property>
<item>
<spacer name="horizontalSpacer_5">
@ -493,8 +479,8 @@
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<width>30</width>
<height>0</height>
</size>
</property>
</spacer>
@ -508,8 +494,8 @@
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<width>0</width>
<height>10</height>
</size>
</property>
</spacer>
@ -525,7 +511,6 @@
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
@ -551,8 +536,8 @@
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<width>0</width>
<height>10</height>
</size>
</property>
</spacer>
@ -566,8 +551,8 @@
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<width>30</width>
<height>0</height>
</size>
</property>
</spacer>
@ -635,13 +620,16 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>editPassword</tabstop>
<tabstop>keyFileLineEdit</tabstop>
<tabstop>buttonBrowseFile</tabstop>
<tabstop>challengeResponseCombo</tabstop>
<tabstop>buttonRedetectYubikey</tabstop>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>useHardwareKeyCheckBox</tabstop>
<tabstop>hardwareKeyCombo</tabstop>
<tabstop>refreshHardwareKeys</tabstop>
<tabstop>addKeyFileLinkLabel</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -16,21 +16,30 @@
*/
#include "YubiKeyEditWidget.h"
#include "ui_KeyComponentWidget.h"
#include "ui_YubiKeyEditWidget.h"
#include "config-keepassx.h"
#include "core/AsyncTask.h"
#include "gui/Icons.h"
#include "keys/ChallengeResponseKey.h"
#include "keys/CompositeKey.h"
#ifdef WITH_XC_YUBIKEY
#include "keys/drivers/YubiKeyInterfaceUSB.h"
#endif
YubiKeyEditWidget::YubiKeyEditWidget(QWidget* parent)
: KeyComponentWidget(parent)
, m_compUi(new Ui::YubiKeyEditWidget())
#ifdef WITH_XC_YUBIKEY
, m_deviceListener(new DeviceListener(this))
#endif
{
initComponent();
#ifdef WITH_XC_YUBIKEY
connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection);
connect(m_deviceListener, &DeviceListener::devicePlugged, this, [&](bool, void*, void*) { pollYubikey(); });
#endif
}
YubiKeyEditWidget::~YubiKeyEditWidget()
@ -76,19 +85,48 @@ QWidget* YubiKeyEditWidget::componentEditWidget()
m_compUi->yubikeyProgress->setSizePolicy(sp);
m_compUi->yubikeyProgress->setVisible(false);
#ifdef WITH_XC_YUBIKEY
connect(m_compUi->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey()));
pollYubikey();
#endif
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)
{
Q_UNUSED(widget);
Q_ASSERT(m_compEditWidget);
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()
@ -118,9 +156,9 @@ void YubiKeyEditWidget::pollYubikey()
m_isDetected = false;
m_compUi->comboChallengeResponse->clear();
m_compUi->comboChallengeResponse->addItem(tr("Detecting hardware keys…"));
m_compUi->buttonRedetectYubikey->setEnabled(false);
m_compUi->comboChallengeResponse->setEnabled(false);
m_compUi->yubikeyProgress->setVisible(true);
m_compUi->refreshHardwareKeys->setEnabled(false);
YubiKey::instance()->findValidKeysAsync();
#endif
@ -133,20 +171,22 @@ void YubiKeyEditWidget::hardwareKeyResponse(bool found)
}
m_compUi->comboChallengeResponse->clear();
m_compUi->buttonRedetectYubikey->setEnabled(true);
m_compUi->yubikeyProgress->setVisible(false);
m_compUi->refreshHardwareKeys->setEnabled(true);
if (!found) {
m_compUi->yubikeyProgress->setVisible(false);
m_compUi->comboChallengeResponse->addItem(tr("No hardware keys detected"));
m_isDetected = false;
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
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_compUi->yubikeyProgress->setVisible(false);
m_compUi->comboChallengeResponse->setEnabled(true);
}

View File

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

View File

@ -24,21 +24,16 @@
<number>0</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout_4">
<property name="verticalSpacing">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QPushButton" name="buttonRedetectYubikey">
<property name="accessibleName">
<string>Refresh hardware tokens</string>
</property>
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item row="0" column="0">
<item>
<widget class="QComboBox" name="comboChallengeResponse">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
@ -51,7 +46,7 @@
</property>
</widget>
</item>
<item row="1" column="0">
<item>
<widget class="QProgressBar" name="yubikeyProgress">
<property name="maximumSize">
<size>
@ -72,6 +67,44 @@
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="refreshHardwareKeys">
<property name="toolTip">
<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>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
@ -87,10 +120,6 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>comboChallengeResponse</tabstop>
<tabstop>buttonRedetectYubikey</tabstop>
</tabstops>
<resources/>
<connections/>
</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,8 +26,8 @@ class ScreenLockListener : public QObject
Q_OBJECT
public:
ScreenLockListener(QWidget* parent = nullptr);
~ScreenLockListener();
explicit ScreenLockListener(QWidget* parent);
~ScreenLockListener() override;
signals:
void screenLocked();

View File

@ -17,7 +17,7 @@
#ifndef SCREENLOCKLISTENERPRIVATE_H
#define SCREENLOCKLISTENERPRIVATE_H
#include <QObject>
#include <QWidget>
class ScreenLockListenerPrivate : public QObject
{
@ -26,7 +26,7 @@ public:
static ScreenLockListenerPrivate* instance(QWidget* parent = nullptr);
protected:
ScreenLockListenerPrivate(QWidget* parent = nullptr);
explicit ScreenLockListenerPrivate(QWidget* parent = nullptr);
signals:
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
#define SCREENLOCKLISTENERDBUS_H
#include "gui/osutils/ScreenLockListenerPrivate.h"
#include <QDBusMessage>
@ -24,7 +25,7 @@ class ScreenLockListenerDBus : public ScreenLockListenerPrivate
{
Q_OBJECT
public:
explicit ScreenLockListenerDBus(QWidget* parent = nullptr);
explicit ScreenLockListenerDBus(QWidget* parent);
private slots:
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
#define SCREENLOCKLISTENERWIN_H
#include <QAbstractNativeEventFilter>
#include <QObject>
#include <QWidget>
#include "gui/osutils/ScreenLockListenerPrivate.h"
@ -27,9 +27,9 @@ class ScreenLockListenerWin : public ScreenLockListenerPrivate, public QAbstract
{
Q_OBJECT
public:
explicit ScreenLockListenerWin(QWidget* parent = nullptr);
explicit ScreenLockListenerWin(QWidget* parent);
~ScreenLockListenerWin();
bool nativeEventFilter(const QByteArray& eventType, void* message, long*) override;
virtual bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
private:
void* m_powerNotificationHandle;

View File

@ -20,10 +20,13 @@
#include "YubiKeyInterfacePCSC.h"
#include "YubiKeyInterfaceUSB.h"
#include <QMutexLocker>
#include <QSet>
#include <QtConcurrent>
QMutex YubiKey::s_interfaceMutex(QMutex::Recursive);
YubiKey::YubiKey()
: m_interfaces_detect_mutex(QMutex::Recursive)
{
int num_interfaces = 0;
@ -70,78 +73,39 @@ bool YubiKey::isInitialized()
bool YubiKey::findValidKeys()
{
bool found = false;
if (m_interfaces_detect_mutex.tryLock(1000)) {
found |= YubiKeyInterfaceUSB::instance()->findValidKeys();
found |= YubiKeyInterfacePCSC::instance()->findValidKeys();
m_interfaces_detect_mutex.unlock();
}
return found;
QMutexLocker lock(&s_interfaceMutex);
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys();
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys();
return !m_usbKeys.isEmpty() || !m_pcscKeys.isEmpty();
}
void YubiKey::findValidKeysAsync()
{
QtConcurrent::run([this] {
bool found = findValidKeys();
emit detectComplete(found);
});
QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
}
QList<YubiKeySlot> YubiKey::foundKeys()
YubiKey::KeyMap YubiKey::foundKeys()
{
QList<YubiKeySlot> foundKeys;
QMutexLocker lock(&s_interfaceMutex);
KeyMap foundKeys;
auto keys = YubiKeyInterfaceUSB::instance()->foundKeys();
QList<unsigned int> handledSerials = keys.uniqueKeys();
for (auto serial : handledSerials) {
for (const auto& key : keys.values(serial)) {
foundKeys.append({serial, key.first});
}
for (auto i = m_usbKeys.cbegin(); i != m_usbKeys.cend(); ++i) {
foundKeys.insert(i.key(), i.value());
}
keys = YubiKeyInterfacePCSC::instance()->foundKeys();
for (auto serial : keys.uniqueKeys()) {
// 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});
}
for (auto i = m_pcscKeys.cbegin(); i != m_pcscKeys.cend(); ++i) {
foundKeys.insert(i.key(), i.value());
}
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()
{
QMutexLocker lock(&s_interfaceMutex);
QString error;
error.clear();
if (!m_error.isNull()) {
@ -177,11 +141,13 @@ QString YubiKey::errorMessage()
*/
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);
}
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
if (m_pcscKeys.contains(slot)) {
return YubiKeyInterfacePCSC::instance()->testChallenge(slot, wouldBlock);
}
@ -200,23 +166,25 @@ bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
YubiKey::ChallengeResult
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
{
QMutexLocker lock(&s_interfaceMutex);
m_error.clear();
// Make sure we tried to find available keys
if (foundKeys().isEmpty()) {
if (m_usbKeys.isEmpty() && m_pcscKeys.isEmpty()) {
findValidKeys();
}
if (YubiKeyInterfaceUSB::instance()->hasFoundKey(slot)) {
if (m_usbKeys.contains(slot)) {
return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
}
if (YubiKeyInterfacePCSC::instance()->hasFoundKey(slot)) {
if (m_pcscKeys.contains(slot)) {
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.")
.arg(slot.first);
return YubiKey::ChallengeResult::YCR_ERROR;
return ChallengeResult::YCR_ERROR;
}

View File

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

View File

@ -19,10 +19,7 @@
#include "YubiKeyInterface.h"
YubiKeyInterface::YubiKeyInterface()
: m_mutex(QMutex::Recursive)
{
m_interactionTimer.setSingleShot(true);
m_interactionTimer.setInterval(300);
}
bool YubiKeyInterface::isInitialized() const
@ -30,36 +27,6 @@ bool YubiKeyInterface::isInitialized() const
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()
{
return m_error;

View File

@ -20,7 +20,6 @@
#define KEEPASSX_YUBIKEY_INTERFACE_H
#include "YubiKey.h"
#include <QMultiMap>
/**
@ -32,11 +31,8 @@ class YubiKeyInterface : public QObject
public:
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
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) = 0;
virtual bool testChallenge(YubiKeySlot slot, bool* wouldBlock) = 0;
@ -60,7 +56,6 @@ signals:
protected:
explicit YubiKeyInterface();
virtual YubiKey::ChallengeResult performChallenge(void* key,
int slot,
bool mayBlock,
@ -68,10 +63,6 @@ protected:
Botan::secure_vector<char>& response) = 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;
QString m_error;

View File

@ -466,40 +466,6 @@ namespace
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
*
@ -575,19 +541,17 @@ YubiKeyInterfacePCSC* YubiKeyInterfacePCSC::instance()
return m_instance;
}
bool YubiKeyInterfacePCSC::findValidKeys()
YubiKey::KeyMap YubiKeyInterfacePCSC::findValidKeys()
{
m_error.clear();
if (!isInitialized()) {
return false;
return {};
}
// Remove all known keys
m_foundKeys.clear();
YubiKey::KeyMap foundKeys;
// Connect to each reader and look for cards
auto readers_list = getReaders(m_sc_context);
foreach (const QString& reader_name, readers_list) {
for (const auto& reader_name : getReaders(m_sc_context)) {
/* Some Yubikeys present their PCSC interface via USB as well
Although this would not be a problem in itself,
we filter these connections because in USB mode,
@ -608,8 +572,12 @@ bool YubiKeyInterfacePCSC::findValidKeys()
&hCard,
&dwActiveProtocol);
if (rv == SCARD_S_SUCCESS) {
// Read the potocol and the ATR record
if (rv != SCARD_S_SUCCESS) {
// Cannot connect to the reader
continue;
}
// Read the protocol and the ATR record
char pbReader[MAX_READERNAME] = {0};
SCUINT dwReaderLen = sizeof(pbReader);
SCUINT dwState = 0;
@ -618,7 +586,11 @@ bool YubiKeyInterfacePCSC::findValidKeys()
SCUINT dwAtrLen = sizeof(pbAtr);
rv = SCardStatus(hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
if (rv == SCARD_S_SUCCESS && (dwProt == SCARD_PROTOCOL_T0 || dwProt == SCARD_PROTOCOL_T1)) {
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;
}
// Find which AID to use
SCardAID satr;
if (findAID(hCard, m_aid_codes, satr)) {
@ -628,12 +600,6 @@ bool YubiKeyInterfacePCSC::findValidKeys()
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;
getSerial(satr, serial);
@ -656,17 +622,20 @@ bool YubiKeyInterfacePCSC::findValidKeys()
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("(PCSC) %1 [%2] Challenge-Response - Slot %3")
.arg(name, QString::number(serial), QString::number(slot));
m_foundKeys.insert(serial, {slot, display});
}
}
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)
@ -707,12 +676,6 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
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
emit challengeStarted();
@ -732,7 +695,6 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
resets the key (see commnt above) */
if (ret == YubiKey::ChallengeResult::YCR_SUCCESS) {
emit challengeCompleted();
m_mutex.unlock();
return ret;
}
}
@ -746,7 +708,6 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
.arg(slot.first)
+ m_error;
emit challengeCompleted();
m_mutex.unlock();
return YubiKey::ChallengeResult::YCR_ERROR;
}

View File

@ -52,7 +52,7 @@ class YubiKeyInterfacePCSC : public YubiKeyInterface
public:
static YubiKeyInterfacePCSC* instance();
bool findValidKeys() override;
YubiKey::KeyMap findValidKeys() override;
YubiKey::ChallengeResult
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override;
@ -71,7 +71,7 @@ private:
Botan::secure_vector<char>& response) 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
// 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 = {
// 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\xFF\x94"),
"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 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\x8C\x80\x01\x59\x75\x62\x69\x6B\x65\x79\x4E\x45\x4F\x72\xFF\x94"), "YubiKey NEO"},
{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\xFF\x7F"), "YubiKey 5"},
{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"),
"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"),
"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"),
@ -104,7 +101,7 @@ private:
"YubiKey NEO"},
{QByteArrayLiteral(
"\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(
"\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)"},

View File

@ -21,7 +21,6 @@
#include "core/Tools.h"
#include "crypto/Random.h"
#include "thirdparty/ykcore/ykcore.h"
#include "thirdparty/ykcore/ykdef.h"
#include "thirdparty/ykcore/ykstatus.h"
namespace
@ -82,7 +81,6 @@ namespace
} // namespace
YubiKeyInterfaceUSB::YubiKeyInterfaceUSB()
: YubiKeyInterface()
{
if (!yk_init()) {
qDebug("YubiKey: Failed to initialize USB interface.");
@ -107,15 +105,14 @@ YubiKeyInterfaceUSB* YubiKeyInterfaceUSB::instance()
return m_instance;
}
bool YubiKeyInterfaceUSB::findValidKeys()
YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
{
m_error.clear();
if (!isInitialized()) {
return false;
return {};
}
// Remove all known keys
m_foundKeys.clear();
YubiKey::KeyMap keyMap;
// Try to detect up to 4 connected hardware keys
for (int i = 0; i < MAX_KEYS; ++i) {
@ -133,13 +130,12 @@ bool YubiKeyInterfaceUSB::findValidKeys()
yk_get_key_vid_pid(yk_key, &vid, &pid);
QString name = m_pid_names.value(pid, tr("Unknown"));
if (vid == 0x1d50) {
name = QStringLiteral("OnlyKey");
if (vid == ONLYKEY_VID) {
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;
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
// if it is enabled for the slot resulting in failed detection
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));
m_foundKeys.insert(serial, {slot, display});
keyMap.insert({serial, slot}, display);
} else if (performTestChallenge(yk_key, slot, &wouldBlock)) {
auto display =
tr("(USB) %1 [%2] Challenge-Response - Slot %3 - %4")
tr("%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"));
m_foundKeys.insert(serial, {slot, display});
keyMap.insert({serial, slot}, display);
}
}
ykds_free(st);
closeKey(yk_key);
Tools::wait(100);
} else if (yk_errno == YK_ENOKEY) {
// No more keys are connected
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) {
ret = performTestChallenge(yk_key, slot.second, wouldBlock);
}
closeKey(yk_key);
return ret;
}
@ -233,18 +228,11 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
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);
if (!yk_key) {
// Key with specified serial number is not connected
m_error =
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;
}
@ -253,7 +241,6 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
closeKey(yk_key);
emit challengeCompleted();
m_mutex.unlock();
return ret;
}

View File

@ -32,8 +32,10 @@ class YubiKeyInterfaceUSB : public YubiKeyInterface
public:
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
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;
// 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"},
{NEO_OTP_PID, "YubiKey NEO - OTP only"},
{NEO_OTP_CCID_PID, "YubiKey NEO - OTP and CCID"},
{NEO_CCID_PID, "YubiKey NEO - CCID only"},
{NEO_U2F_PID, "YubiKey NEO - U2F only"},
{NEO_OTP_U2F_PID, "YubiKey NEO - OTP and U2F"},
{NEO_U2F_CCID_PID, "YubiKey NEO - U2F and CCID"},
{NEO_OTP_U2F_CCID_PID, "YubiKey NEO - OTP, U2F and CCID"},
{YK4_OTP_PID, "YubiKey 4/5 - OTP only"},
{YK4_U2F_PID, "YubiKey 4/5 - U2F only"},
{YK4_OTP_U2F_PID, "YubiKey 4/5 - OTP and U2F"},
{YK4_CCID_PID, "YubiKey 4/5 - CCID only"},
{YK4_OTP_CCID_PID, "YubiKey 4/5 - OTP and CCID"},
{YK4_U2F_CCID_PID, "YubiKey 4/5 - U2F and CCID"},
{YK4_OTP_U2F_CCID_PID, "YubiKey 4/5 - OTP, U2F and CCID"},
{PLUS_U2F_OTP_PID, "YubiKey plus - OTP+U2F"}};
const QHash<int, QString> m_pid_names = {{YUBIKEY_PID, "YubiKey %ver"},
{NEO_OTP_PID, "YubiKey NEO - OTP"},
{NEO_OTP_CCID_PID, "YubiKey NEO - OTP+CCID"},
{NEO_CCID_PID, "YubiKey NEO - CCID"},
{NEO_U2F_PID, "YubiKey NEO - FIDO"},
{NEO_OTP_U2F_PID, "YubiKey NEO - OTP+FIDO"},
{NEO_U2F_CCID_PID, "YubiKey NEO - FIDO+CCID"},
{NEO_OTP_U2F_CCID_PID, "YubiKey NEO - OTP+FIDO+CCID"},
{YK4_OTP_PID, "YubiKey %ver - OTP"},
{YK4_U2F_PID, "YubiKey %ver - U2F"},
{YK4_OTP_U2F_PID, "YubiKey %ver - OTP+FIDO"},
{YK4_CCID_PID, "YubiKey %ver - CCID"},
{YK4_OTP_CCID_PID, "YubiKey %ver - OTP+CCID"},
{YK4_U2F_CCID_PID, "YubiKey %ver - FIDO+CCID"},
{YK4_OTP_U2F_CCID_PID, "YubiKey %ver - OTP+FIDO+CCID"},
{PLUS_U2F_OTP_PID, "YubiKey plus - OTP+FIDO"}};
};
#endif // KEEPASSX_YUBIKEY_INTERFACE_USB_H

View File

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

View File

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

View File

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

View File

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