Skip to content

Commit

Permalink
Automatically detect USB device changes
Browse files Browse the repository at this point in the history
Initial implementation for macOS and Linux
  • Loading branch information
phoerious committed Dec 13, 2023
1 parent 031f7c7 commit a19408c
Show file tree
Hide file tree
Showing 17 changed files with 661 additions and 16 deletions.
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
29 changes: 21 additions & 8 deletions src/gui/DatabaseOpenWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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)));
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 8 additions & 0 deletions src/gui/DatabaseOpenWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@
#ifndef KEEPASSX_DATABASEOPENWIDGET_H
#define KEEPASSX_DATABASEOPENWIDGET_H

#include <QPointer>
#include <QScopedPointer>
#include <QTimer>

#include "config-keepassx.h"
#include "gui/DialogyWidget.h"
#ifdef WITH_XC_YUBIKEY
#include "osutils/DeviceListener.h"
#endif

class CompositeKey;
class Database;
Expand Down Expand Up @@ -78,6 +83,9 @@ private slots:
void openKeyFileHelp();

private:
#ifdef WITH_XC_YUBIKEY
QPointer<DeviceListener> m_deviceListener;
#endif
bool m_pollingHardwareKey = false;
bool m_blockQuickUnlock = false;
bool m_unlockingDatabase = false;
Expand Down
52 changes: 52 additions & 0 deletions src/gui/osutils/DeviceListener.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2023 KeePassXC Team <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "DeviceListener.h"
#include <QTimer>

DeviceListener::DeviceListener(QWidget* parent)
: 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<DEVICELISTENER_IMPL*>(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();
}
74 changes: 74 additions & 0 deletions src/gui/osutils/DeviceListener.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2023 KeePassXC Team <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef DEVICELISTENER_H
#define DEVICELISTENER_H

#include <QScopedPointer>
#include <QWidget>

#if defined(Q_OS_WIN)
#include "winutils/DeviceListenerWin.h"
#elif defined(Q_OS_MACOS)
#include "macutils/DeviceListenerMac.h"
#elif defined(Q_OS_UNIX)
#include "nixutils/DeviceListenerLibUsb.h"
#endif

class QUuid;

class DeviceListener : public 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<QObject> m_platformImpl;
};

#endif // DEVICELISTENER_H
2 changes: 1 addition & 1 deletion src/gui/osutils/ScreenLockListener.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ScreenLockListener : public QObject
Q_OBJECT

public:
ScreenLockListener(QWidget* parent = nullptr);
explicit ScreenLockListener(QWidget* parent);
~ScreenLockListener() override;

signals:
Expand Down
4 changes: 2 additions & 2 deletions src/gui/osutils/ScreenLockListenerPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

#ifndef SCREENLOCKLISTENERPRIVATE_H
#define SCREENLOCKLISTENERPRIVATE_H
#include <QObject>
#include <QWidget>

class ScreenLockListenerPrivate : public QObject
{
Expand All @@ -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();
Expand Down
95 changes: 95 additions & 0 deletions src/gui/osutils/macutils/DeviceListenerMac.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2023 KeePassXC Team <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "DeviceListenerMac.h"

#include <QPointer>
#include <IOKit/IOKitLib.h>

DeviceListenerMac::DeviceListenerMac(QObject* parent)
: QObject(parent)
, m_mgr(nullptr)
{
}

DeviceListenerMac::~DeviceListenerMac()
{
if (m_mgr) {
IOHIDManagerUnscheduleFromRunLoop(m_mgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
IOHIDManagerClose(m_mgr, kIOHIDOptionsTypeNone);
CFRelease(m_mgr);
}
}

void DeviceListenerMac::registerHotplugCallback(bool arrived, bool left, int vendorId, int productId, const QUuid*)
{
if (!m_mgr) {
m_mgr = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDManagerOptionNone);
if (!m_mgr) {
qWarning("Failed to create IOHIDManager.");
return;
}
IOHIDManagerScheduleWithRunLoop(m_mgr, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
}

if (vendorId > 0 || productId > 0) {
CFMutableDictionaryRef matchingDict = IOServiceMatching(kIOHIDDeviceKey);
if (vendorId > 0) {
auto vid = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vendorId);
CFDictionaryAddValue(matchingDict, CFSTR(kIOHIDVendorIDKey), vid);
CFRelease(vid);
}
if (productId > 0) {
auto pid = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vendorId);
CFDictionaryAddValue(matchingDict, CFSTR(kIOHIDProductIDKey), pid);
CFRelease(pid);
}
IOHIDManagerSetDeviceMatching(m_mgr, matchingDict);
CFRelease(matchingDict);
} else {
IOHIDManagerSetDeviceMatching(m_mgr, nullptr);
}

QPointer that = this;
if (arrived) {
IOHIDManagerRegisterDeviceMatchingCallback(m_mgr, [](void* ctx, IOReturn, void*, IOHIDDeviceRef device) {
static_cast<DeviceListenerMac*>(ctx)->onDeviceStateChanged(true, device);
}, that);
}
if (left) {
IOHIDManagerRegisterDeviceRemovalCallback(m_mgr, [](void* ctx, IOReturn, void*, IOHIDDeviceRef device) {
static_cast<DeviceListenerMac*>(ctx)->onDeviceStateChanged(true, device);
}, that);
}

if (IOHIDManagerOpen(m_mgr, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
qWarning("Could not open enumerated devices.");
}
}

void DeviceListenerMac::deregisterHotplugCallback()
{
if (m_mgr) {
IOHIDManagerRegisterDeviceMatchingCallback(m_mgr, nullptr, this);
IOHIDManagerRegisterDeviceRemovalCallback(m_mgr, nullptr, this);
}
}

void DeviceListenerMac::onDeviceStateChanged(bool state, void* device)
{
emit devicePlugged(state, m_mgr, device);
}
Loading

0 comments on commit a19408c

Please sign in to comment.