mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-07-23 06:50:58 -04:00
Significantly enhance hardware key robustness
* Significantly improve user experience when using hardware keys on databases in both GUI and CLI modes. Prevent locking up the YubiKey USB interface for prolonged periods of time. Allows for other apps to use the key concurrently with KeePassXC. * Improve messages displayed to user when finding keys and when user interaction is required. Output specific error messages when handling hardware keys during database read/write. * Only poll for keys when previously used or upon user request. Prevent continuously polling keys when accessing the UI such as switching tabs and minimize/maximize. * Add support for using multiple hardware keys simultaneously. Keys are identified by their serial number which prevents using the wrong key during open and save operations. * Fixes #4400 * Fixes #4065 * Fixes #1050 * Fixes #1215 * Fixes #3087 * Fixes #1088 * Fixes #1869
This commit is contained in:
parent
a145bf9119
commit
5142981018
32 changed files with 670 additions and 687 deletions
|
@ -37,7 +37,6 @@
|
|||
#include <QDesktopServices>
|
||||
#include <QFont>
|
||||
#include <QSharedPointer>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
||||
: DialogyWidget(parent)
|
||||
|
@ -73,18 +72,29 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
|||
connect(m_ui->keyFileClearIcon, SIGNAL(triggered(bool)), SLOT(clearKeyFileEdit()));
|
||||
|
||||
#ifdef WITH_XC_YUBIKEY
|
||||
m_ui->yubikeyProgress->setVisible(false);
|
||||
QSizePolicy sp = m_ui->yubikeyProgress->sizePolicy();
|
||||
m_ui->hardwareKeyProgress->setVisible(false);
|
||||
QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy();
|
||||
sp.setRetainSizeWhenHidden(true);
|
||||
m_ui->yubikeyProgress->setSizePolicy(sp);
|
||||
m_ui->hardwareKeyProgress->setSizePolicy(sp);
|
||||
|
||||
connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey()));
|
||||
connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollHardwareKey()));
|
||||
connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection);
|
||||
|
||||
connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] {
|
||||
// Show the press notification if we are in an independent window (e.g., DatabaseOpenDialog)
|
||||
if (window() != getMainWindow()) {
|
||||
m_ui->messageWidget->showMessage(tr("Please touch the button on your YubiKey!"),
|
||||
MessageWidget::Information,
|
||||
MessageWidget::DisableAutoHide);
|
||||
}
|
||||
});
|
||||
connect(YubiKey::instance(), &YubiKey::challengeCompleted, this, [this] { m_ui->messageWidget->hide(); });
|
||||
#else
|
||||
m_ui->hardwareKeyLabel->setVisible(false);
|
||||
m_ui->hardwareKeyLabelHelp->setVisible(false);
|
||||
m_ui->buttonRedetectYubikey->setVisible(false);
|
||||
m_ui->comboChallengeResponse->setVisible(false);
|
||||
m_ui->yubikeyProgress->setVisible(false);
|
||||
m_ui->challengeResponseCombo->setVisible(false);
|
||||
m_ui->hardwareKeyProgress->setVisible(false);
|
||||
#endif
|
||||
|
||||
#ifndef WITH_XC_TOUCHID
|
||||
|
@ -104,37 +114,16 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event)
|
|||
{
|
||||
DialogyWidget::showEvent(event);
|
||||
m_ui->editPassword->setFocus();
|
||||
|
||||
#ifdef WITH_XC_YUBIKEY
|
||||
// showEvent() may be called twice, so make sure we are only polling once
|
||||
if (!m_yubiKeyBeingPolled) {
|
||||
// clang-format off
|
||||
connect(YubiKey::instance(), SIGNAL(detected(int,bool)), SLOT(yubikeyDetected(int,bool)), Qt::QueuedConnection);
|
||||
connect(YubiKey::instance(), SIGNAL(detectComplete()), SLOT(yubikeyDetectComplete()), Qt::QueuedConnection);
|
||||
connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection);
|
||||
// clang-format on
|
||||
|
||||
pollYubikey();
|
||||
m_yubiKeyBeingPolled = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::hideEvent(QHideEvent* event)
|
||||
{
|
||||
DialogyWidget::hideEvent(event);
|
||||
|
||||
#ifdef WITH_XC_YUBIKEY
|
||||
// Don't listen to any Yubikey events if we are hidden
|
||||
disconnect(YubiKey::instance(), nullptr, this, nullptr);
|
||||
m_yubiKeyBeingPolled = false;
|
||||
#endif
|
||||
|
||||
if (isVisible()) {
|
||||
return;
|
||||
// Clear the forms if we are minimized
|
||||
if (!isVisible()) {
|
||||
clearForms();
|
||||
}
|
||||
|
||||
clearForms();
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::load(const QString& filename)
|
||||
|
@ -148,7 +137,7 @@ void DatabaseOpenWidget::load(const QString& filename)
|
|||
m_keyFileComboEdited = false;
|
||||
|
||||
if (config()->get(Config::RememberLastKeyFiles).toBool()) {
|
||||
QHash<QString, QVariant> lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
|
||||
auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
|
||||
if (lastKeyFiles.contains(m_filename)) {
|
||||
m_ui->comboKeyFile->addItem(lastKeyFiles[m_filename].toString());
|
||||
m_ui->comboKeyFile->setCurrentIndex(1);
|
||||
|
@ -157,6 +146,16 @@ void DatabaseOpenWidget::load(const QString& filename)
|
|||
|
||||
QHash<QString, QVariant> useTouchID = config()->get(Config::UseTouchID).toHash();
|
||||
m_ui->checkTouchID->setChecked(useTouchID.value(m_filename, false).toBool());
|
||||
|
||||
#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();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::clearForms()
|
||||
|
@ -176,6 +175,11 @@ QSharedPointer<Database> DatabaseOpenWidget::database()
|
|||
return m_db;
|
||||
}
|
||||
|
||||
QString DatabaseOpenWidget::filename()
|
||||
{
|
||||
return m_filename;
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
|
||||
{
|
||||
m_ui->editPassword->setText(pw);
|
||||
|
@ -186,6 +190,8 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
|
|||
|
||||
void DatabaseOpenWidget::openDatabase()
|
||||
{
|
||||
m_ui->messageWidget->hide();
|
||||
|
||||
QSharedPointer<CompositeKey> masterKey = databaseKey();
|
||||
if (!masterKey) {
|
||||
return;
|
||||
|
@ -223,11 +229,6 @@ void DatabaseOpenWidget::openDatabase()
|
|||
|
||||
config()->set(Config::UseTouchID, useTouchID);
|
||||
#endif
|
||||
|
||||
if (m_ui->messageWidget->isVisible()) {
|
||||
m_ui->messageWidget->animatedHide();
|
||||
}
|
||||
|
||||
emit dialogFinished(true);
|
||||
m_isOpeningDatabase = false;
|
||||
clearForms();
|
||||
|
@ -293,7 +294,7 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
|
|||
}
|
||||
#endif
|
||||
|
||||
QHash<QString, QVariant> lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
|
||||
auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
|
||||
lastKeyFiles.remove(m_filename);
|
||||
|
||||
auto key = QSharedPointer<FileKey>::create();
|
||||
|
@ -315,14 +316,14 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
|
|||
legacyWarning.setDefaultButton(QMessageBox::Ok);
|
||||
legacyWarning.setCheckBox(new QCheckBox(tr("Don't show this warning again")));
|
||||
|
||||
connect(legacyWarning.checkBox(), &QCheckBox::stateChanged, [](int state) {
|
||||
connect(legacyWarning.checkBox(), &QCheckBox::stateChanged, this, [](int state) {
|
||||
config()->set(Config::Messages_NoLegacyKeyFileWarning, state == Qt::CheckState::Checked);
|
||||
});
|
||||
|
||||
legacyWarning.exec();
|
||||
}
|
||||
masterKey->addKey(key);
|
||||
lastKeyFiles[m_filename] = keyFilename;
|
||||
lastKeyFiles.insert(m_filename, keyFilename);
|
||||
}
|
||||
|
||||
if (config()->get(Config::RememberLastKeyFiles).toBool()) {
|
||||
|
@ -330,19 +331,17 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
|
|||
}
|
||||
|
||||
#ifdef WITH_XC_YUBIKEY
|
||||
QHash<QString, QVariant> lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
|
||||
auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
|
||||
lastChallengeResponse.remove(m_filename);
|
||||
|
||||
int selectionIndex = m_ui->comboChallengeResponse->currentIndex();
|
||||
int selectionIndex = m_ui->challengeResponseCombo->currentIndex();
|
||||
if (selectionIndex > 0) {
|
||||
int comboPayload = m_ui->comboChallengeResponse->itemData(selectionIndex).toInt();
|
||||
|
||||
// read blocking mode from LSB and slot index number from second LSB
|
||||
bool blocking = comboPayload & 1;
|
||||
int slot = comboPayload >> 1;
|
||||
auto crKey = QSharedPointer<YkChallengeResponseKey>(new YkChallengeResponseKey(slot, blocking));
|
||||
auto slot = m_ui->challengeResponseCombo->itemData(selectionIndex).value<YubiKeySlot>();
|
||||
auto crKey = QSharedPointer<YkChallengeResponseKey>(new YkChallengeResponseKey(slot));
|
||||
masterKey->addChallengeResponseKey(crKey);
|
||||
lastChallengeResponse[m_filename] = true;
|
||||
|
||||
// Qt doesn't read custom types in settings so stuff into a QString
|
||||
lastChallengeResponse.insert(m_filename, QStringLiteral("%1:%2").arg(slot.first).arg(slot.second));
|
||||
}
|
||||
|
||||
if (config()->get(Config::RememberLastKeyFiles).toBool()) {
|
||||
|
@ -400,45 +399,62 @@ void DatabaseOpenWidget::handleKeyFileComboChanged()
|
|||
m_ui->keyFileClearIcon->setVisible(m_keyFileComboEdited);
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::pollYubikey()
|
||||
void DatabaseOpenWidget::pollHardwareKey()
|
||||
{
|
||||
m_ui->buttonRedetectYubikey->setEnabled(false);
|
||||
m_ui->comboChallengeResponse->setEnabled(false);
|
||||
m_ui->comboChallengeResponse->clear();
|
||||
m_ui->comboChallengeResponse->addItem(tr("Select slot..."), -1);
|
||||
m_ui->yubikeyProgress->setVisible(true);
|
||||
if (m_pollingHardwareKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// YubiKey init is slow, detect asynchronously to not block the UI
|
||||
QtConcurrent::run(YubiKey::instance(), &YubiKey::detect);
|
||||
m_ui->challengeResponseCombo->clear();
|
||||
m_ui->challengeResponseCombo->addItem(tr("Detecting hardware keys…"));
|
||||
|
||||
m_ui->buttonRedetectYubikey->setEnabled(false);
|
||||
m_ui->challengeResponseCombo->setEnabled(false);
|
||||
m_ui->hardwareKeyProgress->setVisible(true);
|
||||
m_pollingHardwareKey = true;
|
||||
|
||||
YubiKey::instance()->findValidKeys();
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::yubikeyDetected(int slot, bool blocking)
|
||||
void DatabaseOpenWidget::hardwareKeyResponse(bool found)
|
||||
{
|
||||
YkChallengeResponseKey yk(slot, blocking);
|
||||
// add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB
|
||||
m_ui->comboChallengeResponse->addItem(yk.getName(), QVariant((slot << 1) | blocking));
|
||||
m_ui->challengeResponseCombo->clear();
|
||||
m_ui->buttonRedetectYubikey->setEnabled(true);
|
||||
m_ui->hardwareKeyProgress->setVisible(false);
|
||||
m_pollingHardwareKey = false;
|
||||
|
||||
if (!found) {
|
||||
m_ui->challengeResponseCombo->addItem(tr("No hardware keys detected"));
|
||||
m_ui->challengeResponseCombo->setEnabled(false);
|
||||
return;
|
||||
} else {
|
||||
m_ui->challengeResponseCombo->addItem(tr("Select hardware key…"));
|
||||
}
|
||||
|
||||
YubiKeySlot lastUsedSlot;
|
||||
if (config()->get(Config::RememberLastKeyFiles).toBool()) {
|
||||
QHash<QString, QVariant> lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
|
||||
auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash();
|
||||
if (lastChallengeResponse.contains(m_filename)) {
|
||||
m_ui->comboChallengeResponse->setCurrentIndex(1);
|
||||
// Qt doesn't read custom types in settings so extract from QString
|
||||
auto split = lastChallengeResponse.value(m_filename).toString().split(":");
|
||||
if (split.size() > 1) {
|
||||
lastUsedSlot = YubiKeySlot(split[0].toUInt(), split[1].toInt());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::yubikeyDetectComplete()
|
||||
{
|
||||
m_ui->comboChallengeResponse->setEnabled(true);
|
||||
m_ui->buttonRedetectYubikey->setEnabled(true);
|
||||
m_ui->yubikeyProgress->setVisible(false);
|
||||
m_yubiKeyBeingPolled = false;
|
||||
}
|
||||
int selectedIndex = 0;
|
||||
for (auto& slot : YubiKey::instance()->foundKeys()) {
|
||||
// add detected YubiKey to combo box
|
||||
m_ui->challengeResponseCombo->addItem(YubiKey::instance()->getDisplayName(slot), QVariant::fromValue(slot));
|
||||
// Select this YubiKey + Slot if we used it in the past
|
||||
if (lastUsedSlot == slot) {
|
||||
selectedIndex = m_ui->challengeResponseCombo->count() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::noYubikeyFound()
|
||||
{
|
||||
m_ui->buttonRedetectYubikey->setEnabled(true);
|
||||
m_ui->yubikeyProgress->setVisible(false);
|
||||
m_yubiKeyBeingPolled = false;
|
||||
m_ui->challengeResponseCombo->setCurrentIndex(selectedIndex);
|
||||
m_ui->challengeResponseCombo->setEnabled(true);
|
||||
}
|
||||
|
||||
void DatabaseOpenWidget::openHardwareKeyHelp()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue