Automatically detect USB device changes

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

View File

@ -570,6 +570,12 @@ include_directories(SYSTEM ${ZLIB_INCLUDE_DIR})
if(WITH_XC_YUBIKEY)
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

@ -1500,39 +1500,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>&lt;p&gt;In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database&apos;s security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;br&gt;If you do not have a key file, leave this field empty.&lt;/p&gt;&lt;p&gt;Click for more information&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Key file help</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key slot selection</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware Key:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;p&gt;You can use a hardware security key such as a &lt;strong&gt;YubiKey&lt;/strong&gt; or &lt;strong&gt;OnlyKey&lt;/strong&gt; with slots configured for HMAC-SHA1.&lt;/p&gt;
&lt;p&gt;Click for more information&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key help</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Key file to unlock the database</source>
<translation type="unfinished"></translation>
@ -1545,14 +1516,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>
@ -1638,23 +1601,6 @@ To prevent this error from appearing, you must go to &quot;Database Settings / S
<source>Cannot use database file as key file</source>
<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>authenticate to access the database</source>
<translation type="unfinished"></translation>
@ -1663,6 +1609,54 @@ If you do not have a key file, please leave the field empty.</source>
<source>Failed to authenticate with Quick Unlock: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Select Key File:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;p&gt;In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database&apos;s security settings.&lt;/p&gt;&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; your *.kdbx database file!&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Click to add a key file.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;a href=&quot;#&quot; style=&quot;text-decoration: underline&quot;&gt;I have a key file&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use hardware key [Serial: %1]</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Use hardware key</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Your database file is NOT a key file!
If you don&apos;t have a key file or don&apos;t know what that is, you don&apos;t have to select one.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>KeePassXC database file selected</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The file you selected looks like a database file.
A database file is NOT a key file!
Are you sure you want to continue with this file?.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No hardware keys found.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Refresh Hardware Keys</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@ -9633,10 +9627,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>
@ -9648,14 +9638,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>
@ -9700,28 +9682,17 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>&lt;p&gt;If you own a &lt;a href=&quot;https://www.yubico.com/&quot;&gt;YubiKey&lt;/a&gt; or &lt;a href=&quot;https://onlykey.io&quot;&gt;OnlyKey&lt;/a&gt;, you can use it for additional security.&lt;/p&gt;&lt;p&gt;The key requires one of its slots to be programmed as &lt;a href=&quot;https://docs.yubico.com/yesdk/users-manual/application-otp/challenge-response.html&quot;&gt;HMAC-SHA1 Challenge-Response&lt;/a&gt;.&lt;/p&gt;</source>
<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>
@ -9738,6 +9709,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>
@ -9745,14 +9731,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>
@ -9767,10 +9745,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>
@ -9787,5 +9761,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

@ -253,6 +253,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)
@ -401,7 +412,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
#include "quickunlock/QuickUnlockInterface.h"
#include <QCheckBox>
@ -58,6 +60,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);
@ -90,18 +95,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] {
@ -113,12 +127,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
@ -129,6 +148,32 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
DatabaseOpenWidget::~DatabaseOpenWidget() = default;
void DatabaseOpenWidget::toggleKeyFileComponent(bool state)
{
m_ui->addKeyFileLinkLabel->setVisible(!state);
m_ui->selectKeyFileComponent->setVisible(state);
}
void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state)
{
m_ui->hardwareKeyProgress->setVisible(false);
m_ui->hardwareKeyComponent->setVisible(state);
m_ui->hardwareKeyCombo->setVisible(state && m_ui->hardwareKeyCombo->count() != 1);
m_ui->noHardwareKeysFoundLabel->setVisible(!state && m_manualHardwareKeyRefresh);
if (!state) {
m_ui->useHardwareKeyCheckBox->setChecked(false);
}
if (m_ui->hardwareKeyCombo->count() == 1) {
m_ui->useHardwareKeyCheckBox->setText(
tr("Use hardware key [Serial: %1]")
.arg(m_ui->hardwareKeyCombo->itemData(m_ui->hardwareKeyCombo->currentIndex())
.value<YubiKeySlot>()
.first));
} else {
m_ui->useHardwareKeyCheckBox->setText(tr("Use hardware key"));
}
}
void DatabaseOpenWidget::showEvent(QShowEvent* event)
{
DialogyWidget::showEvent(event);
@ -141,6 +186,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)
@ -151,6 +214,10 @@ void DatabaseOpenWidget::hideEvent(QHideEvent* event)
if (!isVisible()) {
m_hideTimer.start();
}
#ifdef WITH_XC_YUBIKEY
m_deviceListener->deregisterAllHotplugCallbacks();
#endif
}
bool DatabaseOpenWidget::unlockingDatabase()
@ -175,6 +242,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);
}
}
@ -186,13 +254,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)) {
pollHardwareKey();
}
}
// Do initial auto-poll
pollHardwareKey();
#endif
}
@ -204,7 +267,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);
QString error;
@ -383,9 +446,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);
@ -406,55 +469,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);
}
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;
@ -466,31 +539,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,172 +109,223 @@
</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">
<property name="text">
<string>Enter Password:</string>
</property>
<property name="buddy">
<cstring>editPassword</cstring>
<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>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="PasswordWidget" name="editPassword" native="true">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Password field</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="hardwareKeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>4</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="PasswordWidget" name="editPassword" native="true">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
<widget class="QFrame" name="selectKeyFileComponent">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="accessibleName">
<string>Password field</string>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="selectKeyFileComponentLayout">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<widget class="QLabel" name="selectKeyFileLabel">
<property name="text">
<string>Select Key File:</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7" stretch="1,0">
<item>
<widget class="PasswordWidget" name="keyFileLineEdit" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Key file to unlock the database</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonBrowseFile">
<property name="toolTip">
<string>Browse for key file</string>
</property>
<property name="accessibleName">
<string>Browse for key file</string>
</property>
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
<widget class="QFrame" name="addAdditionalKeysComponent">
<property name="lineWidth">
<number>0</number>
</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">
<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="QFrame" name="hardwareKeyComponent">
<layout class="QHBoxLayout" name="hardwareKeyComponentLayout">
<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">
<property name="leftMargin">
<number>0</number>
</property>
<item row="1" column="2">
<widget class="QProgressBar" name="hardwareKeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
</size>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
<property 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 notr="true">Use Hardware Security Key [Serial: 11111111]</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="challengeResponseCombo">
<item>
<widget class="QComboBox" name="hardwareKeyCombo">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<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>
@ -298,165 +336,110 @@
</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>
</widget>
</item>
<item>
<widget class="QToolButton" name="hardwareKeyLabelHelp">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</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">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="PasswordWidget" name="keyFileLineEdit" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Key file to unlock the database</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="buttonBrowseFile">
<property name="toolTip">
<string>Browse for key file</string>
</property>
<property name="accessibleName">
<string>Browse for key file</string>
</property>
<property name="text">
<string>Browse…</string>
</property>
</widget>
</item>
<item row="1" column="4">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="buttonRedetectYubikey">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Refresh hardware tokens</string>
</property>
<property name="accessibleName">
<string>Refresh hardware tokens</string>
</property>
<property name="text">
<string>Refresh</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>2</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</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>40</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<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>
<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() = default;
@ -74,19 +83,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()
@ -116,9 +154,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
@ -131,20 +169,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,51 +24,84 @@
<number>0</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout_4">
<property name="verticalSpacing">
<number>0</number>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</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">
<widget class="QComboBox" name="comboChallengeResponse">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Hardware key slot selection</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QProgressBar" name="yubikeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
</size>
</property>
<property name="maximum">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
<item>
<widget class="QComboBox" name="comboChallengeResponse">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Hardware key slot selection</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="yubikeyProgress">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>2</height>
</size>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="value">
<number>-1</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
<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>
@ -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,7 +26,7 @@ class ScreenLockListener : public QObject
Q_OBJECT
public:
ScreenLockListener(QWidget* parent = nullptr);
explicit ScreenLockListener(QWidget* parent);
~ScreenLockListener() override;
signals:

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,65 +572,70 @@ bool YubiKeyInterfacePCSC::findValidKeys()
&hCard,
&dwActiveProtocol);
if (rv == SCARD_S_SUCCESS) {
// Read the protocol and the ATR record
char pbReader[MAX_READERNAME] = {0};
SCUINT dwReaderLen = sizeof(pbReader);
SCUINT dwState = 0;
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
uint8_t pbAtr[MAX_ATR_SIZE] = {0};
SCUINT dwAtrLen = sizeof(pbAtr);
if (rv != SCARD_S_SUCCESS) {
// Cannot connect to the reader
continue;
}
rv = SCardStatus(hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
if (rv == SCARD_S_SUCCESS && (dwProt == SCARD_PROTOCOL_T0 || dwProt == SCARD_PROTOCOL_T1)) {
// Find which AID to use
SCardAID satr;
if (findAID(hCard, m_aid_codes, satr)) {
// Build the UI name using the display name found in the ATR map
QByteArray atr(reinterpret_cast<char*>(pbAtr), dwAtrLen);
QString name("Unknown Key");
if (m_atr_names.contains(atr)) {
name = m_atr_names.value(atr);
}
// Add the firmware version and the serial number
uint8_t version[3] = {0};
getStatus(satr, version);
name +=
QString(" v%1.%2.%3")
.arg(QString::number(version[0]), QString::number(version[1]), QString::number(version[2]));
// Read the protocol and the ATR record
char pbReader[MAX_READERNAME] = {0};
SCUINT dwReaderLen = sizeof(pbReader);
SCUINT dwState = 0;
SCUINT dwProt = SCARD_PROTOCOL_UNDEFINED;
uint8_t pbAtr[MAX_ATR_SIZE] = {0};
SCUINT dwAtrLen = sizeof(pbAtr);
unsigned int serial = 0;
getSerial(satr, serial);
rv = SCardStatus(hCard, pbReader, &dwReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
if (rv != SCARD_S_SUCCESS || (dwProt != SCARD_PROTOCOL_T0 && dwProt != SCARD_PROTOCOL_T1)) {
// Could not read the ATR record or the protocol is not supported
continue;
}
/* This variable indicates that the key is locked / timed out.
When using the key via NFC, the user has to re-present the key to clear the timeout.
Also, the key can be programmatically reset (see below).
When using the key via USB (where the Yubikey presents as a PCSC reader in itself),
the non-HMAC-SHA1 slots (eg. OTP) are incorrectly recognized as locked HMAC-SHA1 slots.
Due to this conundrum, we exclude "locked" keys from the key enumeration,
but only if the reader is the "virtual yubikey reader device".
This also has the nice side effect of de-duplicating interfaces when a key
Is connected via USB and also accessible via PCSC */
bool wouldBlock = false;
/* When the key is used via NFC, the lock state / time-out is cleared when
the smartcard connection is re-established / the applet is selected
so the next call to performTestChallenge actually clears the lock.
Due to this the key is unlocked, and we display it as such.
When the key times out in the time between the key listing and
the database unlock /save, an interaction request will be displayed. */
for (int slot = 1; slot <= 2; ++slot) {
if (performTestChallenge(&satr, slot, &wouldBlock)) {
auto display = tr("(PCSC) %1 [%2] Challenge-Response - Slot %3")
.arg(name, QString::number(serial), QString::number(slot));
m_foundKeys.insert(serial, {slot, display});
}
}
// Find which AID to use
SCardAID satr;
if (findAID(hCard, m_aid_codes, satr)) {
// Build the UI name using the display name found in the ATR map
QByteArray atr(reinterpret_cast<char*>(pbAtr), dwAtrLen);
QString name("Unknown Key");
if (m_atr_names.contains(atr)) {
name = m_atr_names.value(atr);
}
unsigned int serial = 0;
getSerial(satr, serial);
/* This variable indicates that the key is locked / timed out.
When using the key via NFC, the user has to re-present the key to clear the timeout.
Also, the key can be programmatically reset (see below).
When using the key via USB (where the Yubikey presents as a PCSC reader in itself),
the non-HMAC-SHA1 slots (eg. OTP) are incorrectly recognized as locked HMAC-SHA1 slots.
Due to this conundrum, we exclude "locked" keys from the key enumeration,
but only if the reader is the "virtual yubikey reader device".
This also has the nice side effect of de-duplicating interfaces when a key
Is connected via USB and also accessible via PCSC */
bool wouldBlock = false;
/* When the key is used via NFC, the lock state / time-out is cleared when
the smartcard connection is re-established / the applet is selected
so the next call to performTestChallenge actually clears the lock.
Due to this the key is unlocked, and we display it as such.
When the key times out in the time between the key listing and
the database unlock /save, an interaction request will be displayed. */
for (int slot = 1; slot <= 2; ++slot) {
if (performTestChallenge(&satr, slot, &wouldBlock)) {
auto display =
tr("(NFC) %1 [%2] - Slot %3, %4", "YubiKey display fields")
.arg(name,
QString::number(serial),
QString::number(slot),
wouldBlock ? tr("Press", "USB Challenge-Response Key interaction request")
: tr("Passive", "USB Challenge-Response Key no interaction required"));
foundKeys.insert({serial, slot}, display);
}
}
}
}
return !m_foundKeys.isEmpty();
return foundKeys;
}
bool YubiKeyInterfacePCSC::testChallenge(YubiKeySlot slot, bool* wouldBlock)
@ -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 comment 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

@ -45,17 +45,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

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

View File

@ -2253,7 +2253,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

@ -141,6 +141,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);