From 6d38009885ad9cdf9f3c6de89d278cdd7ac8c201 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 + src/CMakeLists.txt | 13 +- src/gui/DatabaseOpenWidget.cpp | 29 +++-- src/gui/DatabaseOpenWidget.h | 8 ++ src/gui/osutils/DeviceListener.cpp | 52 ++++++++ src/gui/osutils/DeviceListener.h | 74 ++++++++++++ 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 | 112 ++++++++++++++++++ .../osutils/nixutils/DeviceListenerLibUsb.h | 52 ++++++++ .../osutils/nixutils/ScreenLockListenerDBus.h | 3 +- .../osutils/winutils/DeviceListenerWin.cpp | 103 ++++++++++++++++ src/gui/osutils/winutils/DeviceListenerWin.h | 62 ++++++++++ .../osutils/winutils/ScreenLockListenerWin.h | 6 +- vcpkg.json | 5 + 17 files changed, 661 insertions(+), 16 deletions(-) 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 65a8820751..c31b8dca8e 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/src/CMakeLists.txt b/src/CMakeLists.txt index 15b6d947ac..f06e7aa728 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -250,6 +250,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) @@ -399,7 +410,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/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 6ce8403d0c..2280f24dbc 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -19,7 +19,6 @@ #include "DatabaseOpenWidget.h" #include "ui_DatabaseOpenWidget.h" -#include "config-keepassx.h" #include "gui/FileDialog.h" #include "gui/Icons.h" #include "gui/MainWindow.h" @@ -58,6 +57,7 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) : DialogyWidget(parent) , m_ui(new Ui::DatabaseOpenWidget()) , m_db(nullptr) + , m_deviceListener(new DeviceListener(this)) { m_ui->setupUi(this); @@ -90,6 +90,10 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase())); connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); +#ifdef WITH_XC_YUBIKEY + connect(m_deviceListener, SIGNAL(devicePlugged(bool, void*, void*)), this, SLOT(pollHardwareKey())); +#endif + 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))); @@ -141,6 +145,16 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event) m_ui->editPassword->setFocus(); } m_hideTimer.stop(); + +#ifdef WITH_XC_YUBIKEY + constexpr int vid = 0x1050; // Yubico vendor ID +#ifdef Q_OS_WIN + m_deviceListener->registerHotplugCallback( + true, true, vid, DeviceListener::MATCH_ANY, &DeviceListenerWin::DEV_CLS_CCID); +#else + m_deviceListener->registerHotplugCallback(true, true, vid); +#endif +#endif } void DatabaseOpenWidget::hideEvent(QHideEvent* event) @@ -151,6 +165,10 @@ void DatabaseOpenWidget::hideEvent(QHideEvent* event) if (!isVisible()) { m_hideTimer.start(); } + +#ifdef WITH_XC_YUBIKEY + m_deviceListener->deregisterHotplugCallback(); +#endif } bool DatabaseOpenWidget::unlockingDatabase() @@ -186,13 +204,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 } diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index 32b8d54c37..ad16ede2ac 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; @@ -78,6 +83,9 @@ private slots: void openKeyFileHelp(); private: +#ifdef WITH_XC_YUBIKEY + QPointer m_deviceListener; +#endif bool m_pollingHardwareKey = false; bool m_blockQuickUnlock = false; bool m_unlockingDatabase = false; diff --git a/src/gui/osutils/DeviceListener.cpp b/src/gui/osutils/DeviceListener.cpp new file mode 100644 index 0000000000..9e7be0af2d --- /dev/null +++ b/src/gui/osutils/DeviceListener.cpp @@ -0,0 +1,52 @@ +/* + * 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) + : QObject(parent) + , m_platformImpl(new DEVICELISTENER_IMPL(parent)) +{ + connect(impl(), &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::~DeviceListener() +{ +} + +DEVICELISTENER_IMPL* DeviceListener::impl() +{ + return qobject_cast(m_platformImpl.data()); +} + +void DeviceListener::registerHotplugCallback(bool arrived, + bool left, + int vendorId, + int productId, + const QUuid* deviceClass) +{ + impl()->registerHotplugCallback(arrived, left, vendorId, productId, deviceClass); +} + +void DeviceListener::deregisterHotplugCallback() +{ + impl()->deregisterHotplugCallback(); +} diff --git a/src/gui/osutils/DeviceListener.h b/src/gui/osutils/DeviceListener.h new file mode 100644 index 0000000000..20234715eb --- /dev/null +++ b/src/gui/osutils/DeviceListener.h @@ -0,0 +1,74 @@ +/* + * 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 + +#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 QObject +{ + Q_OBJECT + +public: + 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 + */ + void registerHotplugCallback(bool arrived, + bool left, + int vendorId = MATCH_ANY, + int productId = MATCH_ANY, + const QUuid* deviceClass = nullptr); + void deregisterHotplugCallback(); + +signals: + void devicePlugged(bool state, void* ctx, void* device); + +private: + DEVICELISTENER_IMPL* impl(); + QScopedPointer m_platformImpl; +}; + +#endif // DEVICELISTENER_H diff --git a/src/gui/osutils/ScreenLockListener.h b/src/gui/osutils/ScreenLockListener.h index f8a3ceeec9..326bacd2ee 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 8f509280b3..34511f168a 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 @@ class ScreenLockListenerPrivate : public QObject 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 0000000000..41d0ddb422 --- /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 0000000000..dae0886e81 --- /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 0000000000..0f9bc4bf12 --- /dev/null +++ b/src/gui/osutils/nixutils/DeviceListenerLibUsb.cpp @@ -0,0 +1,112 @@ +/* + * 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_callbackRef(0) + , m_completed(false) +{ +} + +DeviceListenerLibUsb::~DeviceListenerLibUsb() +{ + if (m_ctx) { + deregisterHotplugCallback(); + 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 + +void 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; + } + } + + if (m_callbackRef) { + // libusb supports registering multiple callbacks, but other platforms don't. + deregisterHotplugCallback(); + m_callbackRef = 0; + } + + int events = 0; + if (arrived) { + events |= LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED; + } + if (left) { + events |= LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT; + } + + 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, + &m_callbackRef); + if (ret != LIBUSB_SUCCESS) { + qWarning("Failed to register USB listener callback."); + m_callbackRef = 0; + } + + m_completed = false; + m_usbEvents = QtConcurrent::run(handleUsbEvents, static_cast(m_ctx), &m_completed); +} + +void DeviceListenerLibUsb::deregisterHotplugCallback() +{ + if (m_ctx) { + libusb_hotplug_deregister_callback(static_cast(m_ctx), m_callbackRef); + if (m_usbEvents.isRunning()) { + m_completed = true; + m_usbEvents.waitForFinished(); + } + } +} diff --git a/src/gui/osutils/nixutils/DeviceListenerLibUsb.h b/src/gui/osutils/nixutils/DeviceListenerLibUsb.h new file mode 100644 index 0000000000..aab51f23d1 --- /dev/null +++ b/src/gui/osutils/nixutils/DeviceListenerLibUsb.h @@ -0,0 +1,52 @@ +/* + * 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 + +class QUuid; + +class DeviceListenerLibUsb : public QObject +{ + Q_OBJECT + +public: + explicit DeviceListenerLibUsb(QWidget* parent); + DeviceListenerLibUsb(const DeviceListenerLibUsb&) = delete; + ~DeviceListenerLibUsb() 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* m_ctx; + int m_callbackRef; + 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 e8ba127aa0..4ece8134f5 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 0000000000..12039c43ab --- /dev/null +++ b/src/gui/osutils/winutils/DeviceListenerWin.cpp @@ -0,0 +1,103 @@ +/* + * 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(); + } + + m_deviceIdPrefix = R"(\\?\USB#)"; + if (vendorId > 0) { + m_deviceIdPrefix += QString("VID_%1&").arg(vendorId, 0, 16); + if (productId > 0) { + m_deviceIdPrefix += QString("PID_%1&").arg(productId, 0, 16); + } + } + + 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 (name.startsWith(m_deviceIdPrefix)) { + 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 0000000000..1c79abd3d3 --- /dev/null +++ b/src/gui/osutils/winutils/DeviceListenerWin.h @@ -0,0 +1,62 @@ +/* + * 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 + +class DeviceListenerWin : public QObject, public QAbstractNativeEventFilter +{ + Q_OBJECT + +public: + static constexpr QUuid DEV_CLS_CCID = + QUuid(0x50dd5230L, 0xba8a, 0x11d1, 0xbf, 0x5d, 0x00, 0x00, 0xf8, 0x05, 0xf5, 0x30); + static constexpr QUuid DEV_CLS_HID = + QUuid(0x745a17a0L, 0x74d3, 0x11d0, 0xb6, 0xfe, 0x00, 0xa0, 0xc9, 0x0f, 0x57, 0xda); + static constexpr QUuid DEV_CLS_USB = + QUuid(0x88bae032L, 0x5a81, 0x49f0, 0xbc, 0x3d, 0xa4, 0xff, 0x13, 0x82, 0x16, 0xd6); + + 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; + QString m_deviceIdPrefix; +}; + +#endif // DEVICELISTENER_WIN_H diff --git a/src/gui/osutils/winutils/ScreenLockListenerWin.h b/src/gui/osutils/winutils/ScreenLockListenerWin.h index edf6c2936b..e33f0e3543 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/vcpkg.json b/vcpkg.json index 6ee4877f36..baa40686c3 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -19,6 +19,11 @@ "name": "libqrencode", "version>=": "4.1.1" }, + { + "name": "libusb", + "version>=": "1.0.26.11791", + "platform": "linux | freebsd" + }, { "name": "libxi", "version>=": "1.8",