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:
Jonathan White 2020-04-06 08:42:20 -04:00
parent a145bf9119
commit 5142981018
32 changed files with 670 additions and 687 deletions

View file

@ -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()