From 6a273363c4558f4f6fd4ce9553a9413b1127f4bf Mon Sep 17 00:00:00 2001 From: Janek Bevendorff Date: Sun, 10 Dec 2023 19:48:43 +0100 Subject: [PATCH] Automatically detect USB device changes --- CMakeLists.txt | 6 + COPYING | 1 + .../scalable/actions/yubikey-refresh.svg | 10 + share/icons/icons.qrc | 1 + share/translations/keepassxc_en.ts | 164 ++--- src/CMakeLists.txt | 13 +- src/core/Bootstrap.h | 2 + src/core/Translator.cpp | 1 - src/core/Translator.h | 1 + src/gui/Application.h | 1 + src/gui/DatabaseOpenWidget.cpp | 186 +++-- src/gui/DatabaseOpenWidget.h | 18 +- src/gui/DatabaseOpenWidget.ui | 640 +++++++++--------- src/gui/databasekey/YubiKeyEditWidget.cpp | 64 +- src/gui/databasekey/YubiKeyEditWidget.h | 9 + src/gui/databasekey/YubiKeyEditWidget.ui | 119 ++-- src/gui/osutils/DeviceListener.cpp | 78 +++ src/gui/osutils/DeviceListener.h | 78 +++ src/gui/osutils/ScreenLockListener.h | 2 +- src/gui/osutils/ScreenLockListenerPrivate.h | 4 +- .../osutils/macutils/DeviceListenerMac.cpp | 95 +++ src/gui/osutils/macutils/DeviceListenerMac.h | 51 ++ .../osutils/nixutils/DeviceListenerLibUsb.cpp | 124 ++++ .../osutils/nixutils/DeviceListenerLibUsb.h | 53 ++ .../osutils/nixutils/ScreenLockListenerDBus.h | 3 +- .../osutils/winutils/DeviceListenerWin.cpp | 105 +++ src/gui/osutils/winutils/DeviceListenerWin.h | 63 ++ .../osutils/winutils/ScreenLockListenerWin.h | 6 +- src/keys/drivers/YubiKey.cpp | 92 +-- src/keys/drivers/YubiKey.h | 11 +- src/keys/drivers/YubiKeyInterface.cpp | 33 - src/keys/drivers/YubiKeyInterface.h | 11 +- src/keys/drivers/YubiKeyInterfacePCSC.cpp | 161 ++--- src/keys/drivers/YubiKeyInterfacePCSC.h | 19 +- src/keys/drivers/YubiKeyInterfaceUSB.cpp | 41 +- src/keys/drivers/YubiKeyInterfaceUSB.h | 36 +- src/keys/drivers/YubiKeyStub.cpp | 8 +- src/quickunlock/WindowsHello.h | 2 +- tests/TestCli.cpp | 2 +- tests/TestYkChallengeResponseKey.cpp | 11 +- tests/gui/TestGui.cpp | 1 + 41 files changed, 1503 insertions(+), 823 deletions(-) create mode 100644 share/icons/application/scalable/actions/yubikey-refresh.svg create mode 100644 src/gui/osutils/DeviceListener.cpp create mode 100644 src/gui/osutils/DeviceListener.h create mode 100644 src/gui/osutils/macutils/DeviceListenerMac.cpp create mode 100644 src/gui/osutils/macutils/DeviceListenerMac.h create mode 100644 src/gui/osutils/nixutils/DeviceListenerLibUsb.cpp create mode 100644 src/gui/osutils/nixutils/DeviceListenerLibUsb.h create mode 100644 src/gui/osutils/winutils/DeviceListenerWin.cpp create mode 100644 src/gui/osutils/winutils/DeviceListenerWin.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fb27aeb6..c8b4da2be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/COPYING b/COPYING index 3da23d212..480b01f07 100644 --- a/COPYING +++ b/COPYING @@ -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 diff --git a/share/icons/application/scalable/actions/yubikey-refresh.svg b/share/icons/application/scalable/actions/yubikey-refresh.svg new file mode 100644 index 000000000..4e51f88a1 --- /dev/null +++ b/share/icons/application/scalable/actions/yubikey-refresh.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index 92753125c..78980d971 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -87,6 +87,7 @@ application/scalable/actions/username-copy.svg application/scalable/actions/view-history.svg application/scalable/actions/web.svg + application/scalable/actions/yubikey-refresh.svg application/scalable/apps/freedesktop.svg application/scalable/apps/internet-web-browser.svg application/scalable/apps/keepassxc.svg diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index ebc1be953..1158a877c 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -1500,39 +1500,10 @@ Backup database located at %2 Password field - - Enter Additional Credentials (if any): - - - - Key File: - - - - <p>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.</p><p>This is <strong>not</strong> your *.kdbx database file!<br>If you do not have a key file, leave this field empty.</p><p>Click for more information…</p> - - - - Key file help - - Hardware key slot selection - - Hardware Key: - - - - <p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> -<p>Click for more information…</p> - - - - Hardware key help - - Key file to unlock the database @@ -1545,14 +1516,6 @@ Backup database located at %2 Browse… - - Refresh hardware tokens - - - - Refresh - - Unlock Database @@ -1638,23 +1601,6 @@ To prevent this error from appearing, you must go to "Database Settings / S Cannot use database file as key file - - You cannot use your database file as a key file. -If you do not have a key file, please leave the field empty. - - - - Detecting hardware keys… - - - - No hardware keys detected - - - - Select hardware key… - - authenticate to access the database @@ -1663,6 +1609,54 @@ If you do not have a key file, please leave the field empty. Failed to authenticate with Quick Unlock: %1 + + Select Key File: + + + + <p>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.</p><p>This is <strong>not</strong> your *.kdbx database file!</p> + + + + Click to add a key file. + + + + <a href="#" style="text-decoration: underline">I have a key file</a> + + + + Use hardware key [Serial: %1] + + + + Use hardware key + + + + Your database file is NOT a key file! +If you don't have a key file or don't know what that is, you don't have to select one. + + + + KeePassXC database file selected + + + + 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?. + + + + No hardware keys found. + + + + Refresh Hardware Keys + + DatabaseSettingWidgetMetaData @@ -9633,10 +9627,6 @@ Example: JBSWY3DPEHPK3PXP YubiKey - - %1 No interface, slot %2 - - General: @@ -9648,14 +9638,6 @@ Example: JBSWY3DPEHPK3PXP YubiKeyEditWidget - - Refresh hardware tokens - - - - Refresh - - Hardware key slot selection @@ -9700,28 +9682,17 @@ Example: JBSWY3DPEHPK3PXP <p>If you own a <a href="https://www.yubico.com/">YubiKey</a> or <a href="https://onlykey.io">OnlyKey</a>, you can use it for additional security.</p><p>The key requires one of its slots to be programmed as <a href="https://docs.yubico.com/yesdk/users-manual/application-otp/challenge-response.html">HMAC-SHA1 Challenge-Response</a>.</p> - - - YubiKeyInterface - %1 Invalid slot specified - %2 + Refresh hardware keys YubiKeyInterfacePCSC - - (PCSC) %1 [%2] Challenge-Response - Slot %3 - - The YubiKey PCSC interface has not been initialized. - - Hardware key is currently in use. - - Could not find or access hardware key with serial number %1. Please present it to continue. @@ -9738,6 +9709,21 @@ Example: JBSWY3DPEHPK3PXP Failed to complete a challenge-response, the PCSC error code was: %1 + + (NFC) %1 [%2] - Slot %3, %4 + YubiKey display fields + + + + Press + USB Challenge-Response Key interaction request + + + + Passive + USB Challenge-Response Key no interaction required + + YubiKeyInterfaceUSB @@ -9745,14 +9731,6 @@ Example: JBSWY3DPEHPK3PXP Unknown - - (USB) %1 [%2] Configured Slot - %3 - - - - (USB) %1 [%2] Challenge-Response - Slot %3 - %4 - - Press USB Challenge-Response Key interaction request @@ -9767,10 +9745,6 @@ Example: JBSWY3DPEHPK3PXP The YubiKey USB interface has not been initialized. - - Hardware key is currently in use. - - Could not find hardware key with serial number %1. Please plug it in to continue. @@ -9787,5 +9761,15 @@ Example: JBSWY3DPEHPK3PXP Failed to complete a challenge-response, the specific error was: %1 + + %1 [%2] - Slot %3 + YubiKey NEO display fields + + + + %1 [%2] - Slot %3, %4 + YubiKey display fields + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ab02f3278..8f1d6b055 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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() diff --git a/src/core/Bootstrap.h b/src/core/Bootstrap.h index 9ec0c5dc8..f458e05b2 100644 --- a/src/core/Bootstrap.h +++ b/src/core/Bootstrap.h @@ -18,6 +18,8 @@ #ifndef KEEPASSXC_BOOTSTRAP_H #define KEEPASSXC_BOOTSTRAP_H +#include + namespace Bootstrap { void bootstrap(); diff --git a/src/core/Translator.cpp b/src/core/Translator.cpp index ac2072bf4..e8a535447 100644 --- a/src/core/Translator.cpp +++ b/src/core/Translator.cpp @@ -25,7 +25,6 @@ #include #include -#include "config-keepassx.h" #include "core/Config.h" #include "core/Resources.h" diff --git a/src/core/Translator.h b/src/core/Translator.h index af699ac90..c0c5f1d3c 100644 --- a/src/core/Translator.h +++ b/src/core/Translator.h @@ -19,6 +19,7 @@ #define KEEPASSX_TRANSLATOR_H #include +#include class Translator { diff --git a/src/gui/Application.h b/src/gui/Application.h index 9cbf48e59..48928650b 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -21,6 +21,7 @@ #define KEEPASSX_APPLICATION_H #include +#include #include #if defined(Q_OS_WIN) || (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS)) diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 6ce8403d0..10856b149 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -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 @@ -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() + .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 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(); + int selectionIndex = m_ui->hardwareKeyCombo->currentIndex(); + if (m_ui->useHardwareKeyCheckBox->isChecked()) { + auto slot = m_ui->hardwareKeyCombo->itemData(selectionIndex).value(); auto crKey = QSharedPointer(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) diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index 32b8d54c3..bc76f6737 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -19,10 +19,15 @@ #ifndef KEEPASSX_DATABASEOPENWIDGET_H #define KEEPASSX_DATABASEOPENWIDGET_H +#include #include #include +#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 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) }; diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index ad4d4af7a..104ca1959 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -6,8 +6,8 @@ 0 0 - 520 - 436 + 745 + 544 @@ -18,7 +18,7 @@ - + 0 @@ -40,18 +40,6 @@ - - - 500 - 400 - - - - - 700 - 16777215 - - @@ -71,7 +59,6 @@ 12 - 75 true @@ -107,8 +94,8 @@ - 0 - 250 + 650 + 0 @@ -122,172 +109,223 @@ + + 5 + - 20 + 30 - 15 + 25 - 20 + 30 - 15 + 25 - - - Enter Password: - - - editPassword + + + 0 + + + 10 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Enter Password: + + + + + + + 0 + + + + + Qt::StrongFocus + + + Password field + + + + + + + + 16777215 + 4 + + + + 0 + + + 0 + + + -1 + + + false + + + + + + - - - Qt::StrongFocus + + + QFrame::NoFrame - - Password field + + QFrame::Plain + + 0 + + + + 10 + + + 0 + + + 10 + + + 0 + + + 15 + + + + + Select Key File: + + + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + Key file to unlock the database + + + + + + + Browse for key file + + + Browse for key file + + + Browse… + + + + + + - - - Qt::Vertical + + + 0 - - QSizePolicy::Fixed - - - - 20 - 5 - - - - - - - - Enter Additional Credentials (if any): - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 15 - 20 - - - - - - - - 3 - - - + + + QLayout::SetMinimumSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + 5 - - - - Key File: - - - keyFileLineEdit - - - - - - - PointingHandCursor - - - Qt::ClickFocus - - - <p>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.</p><p>This is <strong>not</strong> your *.kdbx database file!<br>If you do not have a key file, leave this field empty.</p><p>Click for more information…</p> - - - Key file help - - - QToolButton { - border: none; - background: none; -} - - - ? - - - - 12 - 12 - - - - QToolButton::InstantPopup - - - - - - - - + 0 - - - - - 16777215 - 2 - - - - 0 - - - 0 - - - -1 - - - false + + 0 + + + 0 + + + 0 + + + + + Use Hardware Security Key [Serial: 11111111] - - + + false - - - 0 - 0 - + + + 200 + 0 + + + + + 300 + 16777215 + Hardware key slot selection @@ -298,165 +336,110 @@ - - - - - 2 - - - - - 5 - - - - - Hardware Key: - - - challengeResponseCombo - - - - - - - PointingHandCursor - - - Qt::ClickFocus - - - <p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> -<p>Click for more information…</p> - - - Hardware key help - - - QToolButton { - border: none; - background: none; -} - - - ? - - - - 12 - 12 - - - - QToolButton::InstantPopup - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 2 - - - - - - - - - - 0 - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - Key file to unlock the database - - - - - - - - - Browse for key file - - - Browse for key file - - - Browse… - - - - - - - 0 - - - - - true - - - Refresh hardware tokens - - - Refresh hardware tokens - - - Refresh - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 2 - - - - - - - - - + + + + + + + 0 + 0 + + + + No hardware keys found. + + + 1 + + + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + PointingHandCursor + + + Refresh Hardware Keys + + + Refresh Hardware Keys + + + QPushButton { background-color: transparent; border: none; } + + + + + + + + + + PointingHandCursor + + + Qt::TabFocus + + + <p>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.</p><p>This is <strong>not</strong> your *.kdbx database file!</p> + + + Click to add a key file. + + + <a href="#" style="text-decoration: underline">I have a key file</a> + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 1 + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse + + + + + + + + + + Qt::Vertical + + + + 0 + 5 + + + + + 0 + - 15 + 25 + + + 5 @@ -474,17 +457,20 @@ + + 0 + - 20 + 10 - 15 + 10 - 20 + 10 - 15 + 10 @@ -493,8 +479,8 @@ - 40 - 20 + 30 + 0 @@ -508,8 +494,8 @@ - 20 - 40 + 0 + 10 @@ -525,7 +511,6 @@ 10 - 75 true @@ -551,8 +536,8 @@ - 20 - 40 + 0 + 10 @@ -566,8 +551,8 @@ - 40 - 20 + 30 + 0 @@ -635,13 +620,16 @@ + quickUnlockButton + resetQuickUnlockButton editPassword keyFileLineEdit buttonBrowseFile - challengeResponseCombo - buttonRedetectYubikey - quickUnlockButton - resetQuickUnlockButton + useHardwareKeyCheckBox + hardwareKeyCombo + refreshHardwareKeys + addKeyFileLinkLabel + buttonBox diff --git a/src/gui/databasekey/YubiKeyEditWidget.cpp b/src/gui/databasekey/YubiKeyEditWidget.cpp index 280a6fda0..031a79bf8 100644 --- a/src/gui/databasekey/YubiKeyEditWidget.cpp +++ b/src/gui/databasekey/YubiKeyEditWidget.cpp @@ -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); } diff --git a/src/gui/databasekey/YubiKeyEditWidget.h b/src/gui/databasekey/YubiKeyEditWidget.h index e21b632a6..fc63f1946 100644 --- a/src/gui/databasekey/YubiKeyEditWidget.h +++ b/src/gui/databasekey/YubiKeyEditWidget.h @@ -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 m_compUi; QPointer m_compEditWidget; +#ifdef WITH_XC_YUBIKEY + QPointer m_deviceListener; +#endif bool m_isDetected = false; }; diff --git a/src/gui/databasekey/YubiKeyEditWidget.ui b/src/gui/databasekey/YubiKeyEditWidget.ui index fa150084b..02881817f 100644 --- a/src/gui/databasekey/YubiKeyEditWidget.ui +++ b/src/gui/databasekey/YubiKeyEditWidget.ui @@ -24,51 +24,84 @@ 0 - - - 0 + + + 6 - - - - Refresh hardware tokens - - - Refresh - - - - - - - - 0 - 0 - - - - Hardware key slot selection - - - - - - - - 16777215 - 2 - - - + + + 0 - - -1 + + + + + 0 + 0 + + + + Hardware key slot selection + + + + + + + + 16777215 + 2 + + + + 0 + + + -1 + + + false + + + + + + + + + 0 - - false - - + + + + Refresh hardware keys + + + Refresh hardware keys + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 2 + + + + + @@ -87,10 +120,6 @@ - - comboChallengeResponse - buttonRedetectYubikey - diff --git a/src/gui/osutils/DeviceListener.cpp b/src/gui/osutils/DeviceListener.cpp new file mode 100644 index 000000000..9946295e9 --- /dev/null +++ b/src/gui/osutils/DeviceListener.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#include "DeviceListener.h" +#include + +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(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(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 +} diff --git a/src/gui/osutils/DeviceListener.h b/src/gui/osutils/DeviceListener.h new file mode 100644 index 000000000..831e6ddca --- /dev/null +++ b/src/gui/osutils/DeviceListener.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#ifndef DEVICELISTENER_H +#define DEVICELISTENER_H + +#include +#include +#include +#include + +#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> m_listeners; + void connectSignals(DEVICELISTENER_IMPL* listener); +}; + +#endif // DEVICELISTENER_H diff --git a/src/gui/osutils/ScreenLockListener.h b/src/gui/osutils/ScreenLockListener.h index f8a3ceeec..326bacd2e 100644 --- a/src/gui/osutils/ScreenLockListener.h +++ b/src/gui/osutils/ScreenLockListener.h @@ -26,7 +26,7 @@ class ScreenLockListener : public QObject Q_OBJECT public: - ScreenLockListener(QWidget* parent = nullptr); + explicit ScreenLockListener(QWidget* parent); ~ScreenLockListener() override; signals: diff --git a/src/gui/osutils/ScreenLockListenerPrivate.h b/src/gui/osutils/ScreenLockListenerPrivate.h index 8f509280b..34511f168 100644 --- a/src/gui/osutils/ScreenLockListenerPrivate.h +++ b/src/gui/osutils/ScreenLockListenerPrivate.h @@ -17,7 +17,7 @@ #ifndef SCREENLOCKLISTENERPRIVATE_H #define SCREENLOCKLISTENERPRIVATE_H -#include +#include 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(); diff --git a/src/gui/osutils/macutils/DeviceListenerMac.cpp b/src/gui/osutils/macutils/DeviceListenerMac.cpp new file mode 100644 index 000000000..41d0ddb42 --- /dev/null +++ b/src/gui/osutils/macutils/DeviceListenerMac.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#include "DeviceListenerMac.h" + +#include +#include + +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(ctx)->onDeviceStateChanged(true, device); + }, that); + } + if (left) { + IOHIDManagerRegisterDeviceRemovalCallback(m_mgr, [](void* ctx, IOReturn, void*, IOHIDDeviceRef device) { + static_cast(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); +} diff --git a/src/gui/osutils/macutils/DeviceListenerMac.h b/src/gui/osutils/macutils/DeviceListenerMac.h new file mode 100644 index 000000000..dae0886e8 --- /dev/null +++ b/src/gui/osutils/macutils/DeviceListenerMac.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#ifndef DEVICELISTENER_MAC_H +#define DEVICELISTENER_MAC_H + +#define DEVICELISTENER_IMPL DeviceListenerMac + +#include +#include + +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 diff --git a/src/gui/osutils/nixutils/DeviceListenerLibUsb.cpp b/src/gui/osutils/nixutils/DeviceListenerLibUsb.cpp new file mode 100644 index 000000000..6cbb4a33e --- /dev/null +++ b/src/gui/osutils/nixutils/DeviceListenerLibUsb.cpp @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#include "DeviceListenerLibUsb.h" +#include "core/Tools.h" + +#include +#include +#include + +DeviceListenerLibUsb::DeviceListenerLibUsb(QWidget* parent) + : QObject(parent) + , m_ctx(nullptr) + , m_completed(false) +{ +} + +DeviceListenerLibUsb::~DeviceListenerLibUsb() +{ + if (m_ctx) { + deregisterAllHotplugCallbacks(); + libusb_exit(static_cast(m_ctx)); + m_ctx = nullptr; + } +} + +namespace +{ + void handleUsbEvents(libusb_context* ctx, QAtomicInt* completed) + { + while (!*completed) { + libusb_handle_events_completed(ctx, reinterpret_cast(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(&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(m_ctx), + static_cast(events), + static_cast(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(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(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(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()); + } +} diff --git a/src/gui/osutils/nixutils/DeviceListenerLibUsb.h b/src/gui/osutils/nixutils/DeviceListenerLibUsb.h new file mode 100644 index 000000000..13257e21c --- /dev/null +++ b/src/gui/osutils/nixutils/DeviceListenerLibUsb.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#ifndef DEVICELISTENER_LIBUSB_H +#define DEVICELISTENER_LIBUSB_H + +#define DEVICELISTENER_IMPL DeviceListenerLibUsb + +#include +#include +#include +#include + +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 m_callbackHandles; + QFuture m_usbEvents; + QAtomicInt m_completed; +}; + +#endif // DEVICELISTENER_LIBUSB_H diff --git a/src/gui/osutils/nixutils/ScreenLockListenerDBus.h b/src/gui/osutils/nixutils/ScreenLockListenerDBus.h index e8ba127aa..4ece8134f 100644 --- a/src/gui/osutils/nixutils/ScreenLockListenerDBus.h +++ b/src/gui/osutils/nixutils/ScreenLockListenerDBus.h @@ -17,6 +17,7 @@ #ifndef SCREENLOCKLISTENERDBUS_H #define SCREENLOCKLISTENERDBUS_H + #include "gui/osutils/ScreenLockListenerPrivate.h" #include @@ -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); diff --git a/src/gui/osutils/winutils/DeviceListenerWin.cpp b/src/gui/osutils/winutils/DeviceListenerWin.cpp new file mode 100644 index 000000000..dfd1b610d --- /dev/null +++ b/src/gui/osutils/winutils/DeviceListenerWin.cpp @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#include "DeviceListenerWin.h" + +#include + +#include +#include + +#include + +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(qobject_cast(parent())->winId()); + m_deviceNotifyHandle = RegisterDeviceNotificationW(w, ¬ificationFilter, 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(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(m->lParam); + const auto pDevIface = reinterpret_cast(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; +} diff --git a/src/gui/osutils/winutils/DeviceListenerWin.h b/src/gui/osutils/winutils/DeviceListenerWin.h new file mode 100644 index 000000000..527555d2f --- /dev/null +++ b/src/gui/osutils/winutils/DeviceListenerWin.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * 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 . + */ + +#ifndef DEVICELISTENER_WIN_H +#define DEVICELISTENER_WIN_H + +#define DEVICELISTENER_IMPL DeviceListenerWin + +#include +#include +#include +#include + +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 diff --git a/src/gui/osutils/winutils/ScreenLockListenerWin.h b/src/gui/osutils/winutils/ScreenLockListenerWin.h index edf6c2936..e33f0e354 100644 --- a/src/gui/osutils/winutils/ScreenLockListenerWin.h +++ b/src/gui/osutils/winutils/ScreenLockListenerWin.h @@ -17,8 +17,8 @@ #ifndef SCREENLOCKLISTENERWIN_H #define SCREENLOCKLISTENERWIN_H + #include -#include #include #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; diff --git a/src/keys/drivers/YubiKey.cpp b/src/keys/drivers/YubiKey.cpp index f735c5dba..cac5283d8 100644 --- a/src/keys/drivers/YubiKey.cpp +++ b/src/keys/drivers/YubiKey.cpp @@ -20,10 +20,13 @@ #include "YubiKeyInterfacePCSC.h" #include "YubiKeyInterfaceUSB.h" +#include +#include #include +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 YubiKey::foundKeys() +YubiKey::KeyMap YubiKey::foundKeys() { - QList foundKeys; + QMutexLocker lock(&s_interfaceMutex); + KeyMap foundKeys; - auto keys = YubiKeyInterfaceUSB::instance()->foundKeys(); - QList 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& 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; } diff --git a/src/keys/drivers/YubiKey.h b/src/keys/drivers/YubiKey.h index 312dea897..0eab12986 100644 --- a/src/keys/drivers/YubiKey.h +++ b/src/keys/drivers/YubiKey.h @@ -20,6 +20,7 @@ #define KEEPASSX_YUBIKEY_H #include +#include #include #include #include @@ -36,6 +37,7 @@ class YubiKey : public QObject Q_OBJECT public: + typedef QMap KeyMap; enum class ChallengeResult : int { YCR_ERROR = 0, @@ -49,8 +51,7 @@ public: bool findValidKeys(); void findValidKeysAsync(); - QList foundKeys(); - QString getDisplayName(YubiKeySlot slot); + KeyMap foundKeys(); ChallengeResult challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector& 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) }; diff --git a/src/keys/drivers/YubiKeyInterface.cpp b/src/keys/drivers/YubiKeyInterface.cpp index fe7f984b7..5f7487a91 100644 --- a/src/keys/drivers/YubiKeyInterface.cpp +++ b/src/keys/drivers/YubiKeyInterface.cpp @@ -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> 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; diff --git a/src/keys/drivers/YubiKeyInterface.h b/src/keys/drivers/YubiKeyInterface.h index 6a7294616..51b4ae846 100644 --- a/src/keys/drivers/YubiKeyInterface.h +++ b/src/keys/drivers/YubiKeyInterface.h @@ -20,7 +20,6 @@ #define KEEPASSX_YUBIKEY_INTERFACE_H #include "YubiKey.h" - #include /** @@ -32,11 +31,8 @@ class YubiKeyInterface : public QObject public: bool isInitialized() const; - QMultiMap> 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& 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& response) = 0; virtual bool performTestChallenge(void* key, int slot, bool* wouldBlock) = 0; - QMultiMap> m_foundKeys; - - QMutex m_mutex; - QTimer m_interactionTimer; bool m_initialized = false; QString m_error; diff --git a/src/keys/drivers/YubiKeyInterfacePCSC.cpp b/src/keys/drivers/YubiKeyInterfacePCSC.cpp index 6a35d86e5..41d3c1d66 100644 --- a/src/keys/drivers/YubiKeyInterfacePCSC.cpp +++ b/src/keys/drivers/YubiKeyInterfacePCSC.cpp @@ -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(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(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; } diff --git a/src/keys/drivers/YubiKeyInterfacePCSC.h b/src/keys/drivers/YubiKeyInterfacePCSC.h index 18de5b116..df4f25ba3 100644 --- a/src/keys/drivers/YubiKeyInterfacePCSC.h +++ b/src/keys/drivers/YubiKeyInterfacePCSC.h @@ -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& response) override; @@ -71,7 +71,7 @@ private: Botan::secure_vector& 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 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)"}, diff --git a/src/keys/drivers/YubiKeyInterfaceUSB.cpp b/src/keys/drivers/YubiKeyInterfaceUSB.cpp index ffbceeebb..80d200f91 100644 --- a/src/keys/drivers/YubiKeyInterfaceUSB.cpp +++ b/src/keys/drivers/YubiKeyInterfaceUSB.cpp @@ -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; } diff --git a/src/keys/drivers/YubiKeyInterfaceUSB.h b/src/keys/drivers/YubiKeyInterfaceUSB.h index 885188615..07c8118b7 100644 --- a/src/keys/drivers/YubiKeyInterfaceUSB.h +++ b/src/keys/drivers/YubiKeyInterfaceUSB.h @@ -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& 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 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 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 diff --git a/src/keys/drivers/YubiKeyStub.cpp b/src/keys/drivers/YubiKeyStub.cpp index d897ccd56..04e7cb118 100644 --- a/src/keys/drivers/YubiKeyStub.cpp +++ b/src/keys/drivers/YubiKeyStub.cpp @@ -45,17 +45,11 @@ void YubiKey::findValidKeysAsync() { } -QList YubiKey::foundKeys() +YubiKey::KeyMap YubiKey::foundKeys() { return {}; } -QString YubiKey::getDisplayName(YubiKeySlot slot) -{ - Q_UNUSED(slot); - return {}; -} - QString YubiKey::errorMessage() { return {}; diff --git a/src/quickunlock/WindowsHello.h b/src/quickunlock/WindowsHello.h index ea59f91c3..9da6e4160 100644 --- a/src/quickunlock/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -18,7 +18,7 @@ #ifndef KEEPASSXC_WINDOWSHELLO_H #define KEEPASSXC_WINDOWSHELLO_H -#include "QuickUnlockInterface.h"; +#include "QuickUnlockInterface.h" #include #include diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 459d5be7e..ba1f84a49 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -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."); } diff --git a/tests/TestYkChallengeResponseKey.cpp b/tests/TestYkChallengeResponseKey.cpp index 341c90715..c1064518e 100644 --- a/tests/TestYkChallengeResponseKey.cpp +++ b/tests/TestYkChallengeResponseKey.cpp @@ -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."); } diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index aaa02bc82..57d1392bc 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -141,6 +141,7 @@ void TestGui::init() databaseOpenWidget->findChild("editPassword")->findChild("passwordEdit"); QVERIFY(editPassword); editPassword->setFocus(); + QTRY_VERIFY(editPassword->hasFocus()); QTest::keyClicks(editPassword, "a"); QTest::keyClick(editPassword, Qt::Key_Enter);