Skip to content

Commit

Permalink
Show a clear error if no slots on hardware key(s) are configured (#11609
Browse files Browse the repository at this point in the history
)

Fixes #11543

Also fix delayed polling on window activation

---------

Co-authored-by: w15dev <w15developer@proton.me>
Co-authored-by: Jonathan White <support@dmapps.us>
  • Loading branch information
3 people authored Jan 3, 2025
1 parent 2afec91 commit 9e29b5c
Show file tree
Hide file tree
Showing 13 changed files with 89 additions and 58 deletions.
8 changes: 8 additions & 0 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,10 @@ Are you sure you want to continue with this file?.</source>
<source>&lt;a href=&quot;#&quot; style=&quot;text-decoration: underline&quot;&gt;I have a key file&lt;/a&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware keys found, but no slots are configured.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
Expand Down Expand Up @@ -10230,6 +10234,10 @@ Example: JBSWY3DPEHPK3PXP</source>
<source>&lt;p&gt;If you own a &lt;a href=&quot;https://www.yubico.com/&quot;&gt;YubiKey&lt;/a&gt; or &lt;a href=&quot;https://onlykey.io&quot;&gt;OnlyKey&lt;/a&gt;, you can use it for additional security.&lt;/p&gt;&lt;p&gt;The key requires one of its slots to be programmed with &lt;a href=&quot;https://keepassxc.org/docs/#faq-yubikey-howto&quot;&gt;Challenge-Response&lt;/a&gt;.&lt;/p&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware keys found, but no slots are configured</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>YubiKeyInterfacePCSC</name>
Expand Down
12 changes: 7 additions & 5 deletions src/gui/DatabaseOpenWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->hardwareKeyProgress->setSizePolicy(sp);

#ifdef WITH_XC_YUBIKEY
connect(m_deviceListener, SIGNAL(devicePlugged(bool, void*, void*)), this, SLOT(pollHardwareKey()));
connect(m_deviceListener, &DeviceListener::devicePlugged, this, [this] { pollHardwareKey(false, 500); });
connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection);

connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] {
Expand Down Expand Up @@ -140,7 +140,12 @@ 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);
m_ui->noHardwareKeysFoundLabel->setText(YubiKey::instance()->connectedKeys() > 0
? tr("Hardware keys found, but no slots are configured.")
: tr("No hardware keys found."));

if (!state) {
m_ui->useHardwareKeyCheckBox->setChecked(false);
}
Expand Down Expand Up @@ -520,7 +525,7 @@ bool DatabaseOpenWidget::browseKeyFile()
return true;
}

void DatabaseOpenWidget::pollHardwareKey(bool manualTrigger)
void DatabaseOpenWidget::pollHardwareKey(bool manualTrigger, int delay)
{
if (m_pollingHardwareKey) {
return;
Expand All @@ -534,9 +539,6 @@ void DatabaseOpenWidget::pollHardwareKey(bool manualTrigger)
m_pollingHardwareKey = true;
m_manualHardwareKeyRefresh = manualTrigger;

// Add a delay, if this is an automatic trigger, to allow the USB device to settle as
// the device may not report a valid serial number immediately after plugging in
int delay = manualTrigger ? 0 : 500;
QTimer::singleShot(delay, this, [] { YubiKey::instance()->findValidKeysAsync(); });
}

Expand Down
2 changes: 1 addition & 1 deletion src/gui/DatabaseOpenWidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ protected slots:
private slots:
bool browseKeyFile();
void toggleHardwareKeyComponent(bool state);
void pollHardwareKey(bool manualTrigger = false);
void pollHardwareKey(bool manualTrigger = false, int delay = 0);
void hardwareKeyResponse(bool found);

private:
Expand Down
4 changes: 3 additions & 1 deletion src/gui/databasekey/YubiKeyEditWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ void YubiKeyEditWidget::hardwareKeyResponse(bool found)

if (!found) {
m_compUi->yubikeyProgress->setVisible(false);
m_compUi->comboChallengeResponse->addItem(tr("No hardware keys detected"));
m_compUi->comboChallengeResponse->addItem(YubiKey::instance()->connectedKeys() > 0
? tr("Hardware keys found, but no slots are configured")
: tr("No hardware keys detected"));
m_isDetected = false;
return;
}
Expand Down
21 changes: 10 additions & 11 deletions src/keys/drivers/YubiKey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ bool YubiKey::findValidKeys()
{
QMutexLocker lock(&s_interfaceMutex);

m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys();
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys();
m_connectedKeys = 0;
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);

return !m_usbKeys.isEmpty() || !m_pcscKeys.isEmpty();
}
Expand All @@ -89,19 +90,17 @@ void YubiKey::findValidKeysAsync()
YubiKey::KeyMap YubiKey::foundKeys()
{
QMutexLocker lock(&s_interfaceMutex);
KeyMap foundKeys;

for (auto i = m_usbKeys.cbegin(); i != m_usbKeys.cend(); ++i) {
foundKeys.insert(i.key(), i.value());
}

for (auto i = m_pcscKeys.cbegin(); i != m_pcscKeys.cend(); ++i) {
foundKeys.insert(i.key(), i.value());
}
KeyMap foundKeys = m_usbKeys;
foundKeys.unite(m_pcscKeys);

return foundKeys;
}

int YubiKey::connectedKeys()
{
return m_connectedKeys;
}

QString YubiKey::errorMessage()
{
QMutexLocker lock(&s_interfaceMutex);
Expand Down
6 changes: 5 additions & 1 deletion src/keys/drivers/YubiKey.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class YubiKey : public QObject
Q_OBJECT

public:
typedef QMap<YubiKeySlot, QString> KeyMap;
using KeyMap = QMap<YubiKeySlot, QString>;

enum class ChallengeResult : int
{
YCR_ERROR = 0,
Expand All @@ -53,6 +54,7 @@ class YubiKey : public QObject
void findValidKeysAsync();

KeyMap foundKeys();
int connectedKeys();

ChallengeResult challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response);
bool testChallenge(YubiKeySlot slot, bool* wouldBlock = nullptr);
Expand Down Expand Up @@ -93,6 +95,8 @@ class YubiKey : public QObject
KeyMap m_usbKeys;
KeyMap m_pcscKeys;

int m_connectedKeys = 0;

Q_DISABLE_COPY(YubiKey)
};

Expand Down
2 changes: 1 addition & 1 deletion src/keys/drivers/YubiKeyInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class YubiKeyInterface : public QObject
public:
bool isInitialized() const;

virtual YubiKey::KeyMap findValidKeys() = 0;
virtual YubiKey::KeyMap findValidKeys(int& connectedKeys) = 0;
virtual YubiKey::ChallengeResult
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) = 0;
virtual bool testChallenge(YubiKeySlot slot, bool* wouldBlock) = 0;
Expand Down
9 changes: 7 additions & 2 deletions src/keys/drivers/YubiKeyInterfacePCSC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include "core/Tools.h"
#include "crypto/Random.h"

#include <QScopeGuard>

// MSYS2 does not define these macros
// So set them to the value used by pcsc-lite
#ifndef MAX_ATR_SIZE
Expand Down Expand Up @@ -511,7 +513,6 @@ namespace
return rv;
});
}

} // namespace

YubiKeyInterfacePCSC::YubiKeyInterfacePCSC()
Expand Down Expand Up @@ -542,7 +543,7 @@ YubiKeyInterfacePCSC* YubiKeyInterfacePCSC::instance()
return m_instance;
}

YubiKey::KeyMap YubiKeyInterfacePCSC::findValidKeys()
YubiKey::KeyMap YubiKeyInterfacePCSC::findValidKeys(int& connectedKeys)
{
m_error.clear();
if (!isInitialized()) {
Expand Down Expand Up @@ -578,6 +579,8 @@ YubiKey::KeyMap YubiKeyInterfacePCSC::findValidKeys()
continue;
}

auto finally = qScopeGuard([hCard]() { SCardDisconnect(hCard, SCARD_LEAVE_CARD); });

// Read the protocol and the ATR record
char pbReader[MAX_READERNAME] = {0};
SCUINT dwReaderLen = sizeof(pbReader);
Expand Down Expand Up @@ -605,6 +608,8 @@ YubiKey::KeyMap YubiKeyInterfacePCSC::findValidKeys()
unsigned int serial = 0;
getSerial(satr, serial);

++connectedKeys;

/* 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).
Expand Down
2 changes: 1 addition & 1 deletion src/keys/drivers/YubiKeyInterfacePCSC.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class YubiKeyInterfacePCSC : public YubiKeyInterface
public:
static YubiKeyInterfacePCSC* instance();

YubiKey::KeyMap findValidKeys() override;
YubiKey::KeyMap findValidKeys(int& connectedKeys) override;

YubiKey::ChallengeResult
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override;
Expand Down
68 changes: 35 additions & 33 deletions src/keys/drivers/YubiKeyInterfaceUSB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

#include "YubiKeyInterfaceUSB.h"

#include "core/Tools.h"
#include "crypto/Random.h"
#include "thirdparty/ykcore/ykcore.h"
#include "thirdparty/ykcore/ykstatus.h"
Expand All @@ -27,7 +26,15 @@ namespace
{
constexpr int MAX_KEYS = 4;

YK_KEY* openKey(int index)
void closeKey(YK_KEY* key)
{
yk_close_key(key);
}

using Yubikey = std::unique_ptr<YK_KEY, decltype(&closeKey)>;
using YubikeyStatus = std::unique_ptr<YK_STATUS, decltype(&ykds_free)>;

Yubikey openKey(int index)
{
static const int vids[] = {YUBICO_VID, ONLYKEY_VID};
static const int pids[] = {YUBIKEY_PID,
Expand All @@ -42,12 +49,9 @@ namespace
PLUS_U2F_OTP_PID,
ONLYKEY_PID};

return yk_open_key_vid_pid(vids, sizeof(vids) / sizeof(vids[0]), pids, sizeof(pids) / sizeof(pids[0]), index);
}

void closeKey(YK_KEY* key)
{
yk_close_key(key);
return Yubikey{
yk_open_key_vid_pid(vids, sizeof(vids) / sizeof(vids[0]), pids, sizeof(pids) / sizeof(pids[0]), index),
&closeKey};
}

void printError()
Expand All @@ -69,24 +73,25 @@ namespace
return serial;
}

YK_KEY* openKeySerial(unsigned int serial)
Yubikey openKeySerial(unsigned int serial)
{
for (int i = 0; i < MAX_KEYS; ++i) {
auto* yk_key = openKey(i);
if (yk_key) {
auto key = openKey(i);
if (key) {
// If the provided serial number is 0, or the key matches the serial, return it
if (serial == 0 || getSerial(yk_key) == serial) {
return yk_key;
if (serial == 0 || getSerial(key.get()) == serial) {
return key;
}
closeKey(yk_key);
} else if (yk_errno == YK_ENOKEY) {
// No more connected keys
break;
}
printError();
}
return nullptr;

return Yubikey{nullptr, &closeKey};
}

} // namespace

YubiKeyInterfaceUSB::YubiKeyInterfaceUSB()
Expand Down Expand Up @@ -114,7 +119,7 @@ YubiKeyInterfaceUSB* YubiKeyInterfaceUSB::instance()
return m_instance;
}

YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys(int& connectedKeys)
{
m_error.clear();
if (!isInitialized()) {
Expand All @@ -127,29 +132,30 @@ YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
for (int i = 0; i < MAX_KEYS; ++i) {
auto yk_key = openKey(i);
if (yk_key) {
auto serial = getSerial(yk_key);
auto serial = getSerial(yk_key.get());
if (serial == 0) {
closeKey(yk_key);
continue;
}

auto st = ykds_alloc();
yk_get_status(yk_key, st);
++connectedKeys;

YubikeyStatus st{ykds_alloc(), &ykds_free};
yk_get_status(yk_key.get(), st.get());
int vid, pid;
yk_get_key_vid_pid(yk_key, &vid, &pid);
yk_get_key_vid_pid(yk_key.get(), &vid, &pid);

QString name = m_pid_names.value(pid, tr("Unknown"));
if (vid == ONLYKEY_VID) {
name = QStringLiteral("OnlyKey %ver");
}
if (name.contains("%ver")) {
name = name.replace("%ver", QString::number(ykds_version_major(st)));
name = name.replace("%ver", QString::number(ykds_version_major(st.get())));
}

bool wouldBlock;
for (int slot = 1; slot <= 2; ++slot) {
auto config = (slot == 1 ? CONFIG1_VALID : CONFIG2_VALID);
if (!(ykds_touch_level(st) & config)) {
if (!(ykds_touch_level(st.get()) & config)) {
// Slot is not configured
continue;
}
Expand All @@ -159,7 +165,7 @@ YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
auto display = tr("%1 [%2] - Slot %3", "YubiKey NEO display fields")
.arg(name, QString::number(serial), QString::number(slot));
keyMap.insert({serial, slot}, display);
} else if (performTestChallenge(yk_key, slot, &wouldBlock)) {
} else if (performTestChallenge(yk_key.get(), slot, &wouldBlock)) {
auto display =
tr("%1 [%2] - Slot %3, %4", "YubiKey display fields")
.arg(name,
Expand All @@ -170,9 +176,6 @@ YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
keyMap.insert({serial, slot}, display);
}
}

ykds_free(st);
closeKey(yk_key);
} else if (yk_errno == YK_ENOKEY) {
// No more keys are connected
break;
Expand All @@ -197,11 +200,11 @@ YubiKey::KeyMap YubiKeyInterfaceUSB::findValidKeys()
bool YubiKeyInterfaceUSB::testChallenge(YubiKeySlot slot, bool* wouldBlock)
{
bool ret = false;
auto* yk_key = openKeySerial(slot.first);
auto yk_key = openKeySerial(slot.first);
if (yk_key) {
ret = performTestChallenge(yk_key, slot.second, wouldBlock);
ret = performTestChallenge(yk_key.get(), slot.second, wouldBlock);
}
closeKey(yk_key);

return ret;
}

Expand Down Expand Up @@ -237,7 +240,7 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
return YubiKey::ChallengeResult::YCR_ERROR;
}

auto* yk_key = openKeySerial(slot.first);
auto yk_key = openKeySerial(slot.first);
if (!yk_key) {
// Key with specified serial number is not connected
m_error =
Expand All @@ -246,9 +249,8 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
}

emit challengeStarted();
auto ret = performChallenge(yk_key, slot.second, true, challenge, response);
auto ret = performChallenge(yk_key.get(), slot.second, true, challenge, response);

closeKey(yk_key);
emit challengeCompleted();

return ret;
Expand Down
2 changes: 1 addition & 1 deletion src/keys/drivers/YubiKeyInterfaceUSB.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class YubiKeyInterfaceUSB : public YubiKeyInterface
static constexpr int YUBICO_USB_VID = YUBICO_VID;
static constexpr int ONLYKEY_USB_VID = ONLYKEY_VID;

YubiKey::KeyMap findValidKeys() override;
YubiKey::KeyMap findValidKeys(int& connectedKeys) override;

YubiKey::ChallengeResult
challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response) override;
Expand Down
Loading

0 comments on commit 9e29b5c

Please sign in to comment.