diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index c83fefc75..6808594e8 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -586,10 +586,6 @@ Convenience - - Enable database quick unlock (Touch ID / Windows Hello) - - Lock databases when session is locked or lid is closed @@ -634,6 +630,18 @@ Hide notes in the entry preview panel + + Quick unlock can only be remembered when using Touch ID or Windows Hello + + + + Enable database quick unlock by default + + + + Remember quick unlock after database is closed (Touch ID / Windows Hello only) + + AutoType @@ -1527,10 +1535,6 @@ Backup database located at %2 Unlock Database - - Cancel - - Unlock @@ -1664,6 +1668,18 @@ Are you sure you want to continue with this file?. <a href="#" style="text-decoration: underline">I have a key file</a> + + Enable Quick Unlock + + + + Reset + + + + Close Database + + DatabaseSettingWidgetMetaData @@ -8852,46 +8868,10 @@ This option is deprecated, use --set-key-file instead. Passkeys - - AES initialization failed - - - - AES encrypt failed - - - - Failed to store in Linux Keyring - - Polkit returned an error: %1 - - Could not locate key in keyring - - - - Could not read key in keyring - - - - AES decrypt failed - - - - No Polkit authentication agent was available - - - - Polkit authorization failed - - - - No Quick Unlock provider is available - - Failed to init KeePassXC crypto. @@ -9073,6 +9053,58 @@ This option is deprecated, use --set-key-file instead. Passkey + + Quick Unlock Pin Entry + + + + Enter a %1 to %2 digit pin to use for quick unlock: + + + + Pin setup was canceled. Quick unlock has not been enabled. + + + + Failed to get credentials for quick unlock. + + + + Enter quick unlock pin (%1 of %2 attempts): + + + + Pin entry was canceled. + + + + Maximum pin attempts have been reached. + + + + Failed to store key in Linux Keyring. Quick unlock has not been enabled. + + + + Could not locate key in Linux Keyring. + + + + Could not read key in Linux Keyring. + + + + No Polkit authentication agent was available. + + + + Polkit authorization failed. + + + + Windows Hello setup was canceled or failed. Quick unlock has not been enabled. + + QtIOCompressor diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ee83fac32..673e61899 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -205,6 +205,7 @@ set(gui_SOURCES gui/wizard/NewDatabaseWizardPageEncryption.cpp gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp quickunlock/QuickUnlockInterface.cpp + quickunlock/PinUnlock.cpp ../share/icons/icons.qrc ../share/wizard/wizard.qrc) diff --git a/src/core/Config.h b/src/core/Config.h index 0ee17b663..1b0e8f855 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -130,6 +130,7 @@ public: Security_EnableCopyOnDoubleClick, Security_QuickUnlock, Security_QuickUnlockRemember, + Security_DatabasePasswordMinimumQuality, Browser_Enabled, Browser_ShowNotification, diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 20b448413..e4869e1f0 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -147,10 +147,6 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) m_secUi->lockDatabaseMinimizeCheckBox->setEnabled(!state); }); - connect(m_secUi->quickUnlockCheckBox, &QCheckBox::toggled, this, [this](bool state) { - m_secUi->quickUnlockRememberCheckBox->setEnabled(state); - }); - // Set Auto-Type shortcut when changed connect( m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutChanged, this, [this](auto key, auto modifiers) { @@ -346,17 +342,12 @@ void ApplicationSettingsWidget::loadSettings() m_secUi->hideTotpCheckBox->setChecked(config()->get(Config::Security_HideTotpPreviewPanel).toBool()); m_secUi->hideNotesCheckBox->setChecked(config()->get(Config::Security_HideNotes).toBool()); - m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable()); m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool()); - m_secUi->quickUnlockCheckBox->setToolTip( - m_secUi->quickUnlockCheckBox->isEnabled() ? QString() : tr("Quick unlock is not available on your device.")); - - m_secUi->quickUnlockRememberCheckBox->setEnabled(getQuickUnlock()->isAvailable() - && getQuickUnlock()->canRemember()); m_secUi->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool()); - m_secUi->quickUnlockRememberCheckBox->setToolTip(m_secUi->quickUnlockRememberCheckBox->isEnabled() - ? QString() - : tr("Quick unlock cannot be remembered on your device.")); +#ifdef Q_OS_LINUX + // Remembering quick unlock is not supported on Linux + m_secUi->quickUnlockRememberCheckBox->setVisible(false); +#endif for (const ExtraPage& page : asConst(m_extraPages)) { page.loadSettings(); @@ -471,10 +462,8 @@ void ApplicationSettingsWidget::saveSettings() config()->set(Config::Security_HideTotpPreviewPanel, m_secUi->hideTotpCheckBox->isChecked()); config()->set(Config::Security_HideNotes, m_secUi->hideNotesCheckBox->isChecked()); - if (m_secUi->quickUnlockCheckBox->isEnabled()) { - config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked()); - config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked()); - } + config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked()); + config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked()); // Security: clear storage if related settings are disabled if (!config()->get(Config::RememberLastDatabases).toBool()) { diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui index aeafce324..f708995a5 100644 --- a/src/gui/ApplicationSettingsWidgetSecurity.ui +++ b/src/gui/ApplicationSettingsWidgetSecurity.ui @@ -6,8 +6,8 @@ 0 0 - 364 - 505 + 437 + 529 @@ -168,39 +168,19 @@ - Enable database quick unlock (Touch ID / Windows Hello / Polkit) + Enable database quick unlock by default - - - 0 + + + Quick unlock can only be remembered when using Touch ID or Windows Hello - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 30 - 0 - - - - - - - - Remember quick unlock after database is closed - - - - + + Remember quick unlock after database is closed (Touch ID / Windows Hello only) + + diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp index fa9383ac2..fc2307ac0 100644 --- a/src/gui/DatabaseOpenDialog.cpp +++ b/src/gui/DatabaseOpenDialog.cpp @@ -84,9 +84,8 @@ void DatabaseOpenDialog::showEvent(QShowEvent* event) { QDialog::showEvent(event); QTimer::singleShot(100, this, [this] { - if (m_view->isOnQuickUnlockScreen() && !m_view->unlockingDatabase()) { - m_view->triggerQuickUnlock(); - } + // Automatically trigger quick unlock if it's available + m_view->triggerQuickUnlock(); }); } diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 3b22eb513..d7d5470cd 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -38,14 +38,6 @@ namespace { constexpr int clearFormsDelay = 30000; - - bool isQuickUnlockAvailable() - { - if (config()->get(Config::Security_QuickUnlock).toBool()) { - return getQuickUnlock()->isAvailable(); - } - return false; - } } // namespace DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) @@ -68,17 +60,10 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) m_ui->editPassword->setShowPassword(false); }); - QFont font; - font.setPointSize(font.pointSize() + 4); - font.setBold(true); - m_ui->labelHeadline->setFont(font); - - m_ui->quickUnlockButton->setFont(font); - m_ui->quickUnlockButton->setIcon( - icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText))); - m_ui->quickUnlockButton->setIconSize({32, 32}); - - connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile())); + QFont largeFont; + largeFont.setPointSize(largeFont.pointSize() + 4); + largeFont.setBold(true); + m_ui->labelHeadline->setFont(largeFont); auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok); okBtn->setText(tr("Unlock")); @@ -86,16 +71,19 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase())); connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); + // Key file components + m_ui->selectKeyFileComponent->setVisible(false); connect(m_ui->addKeyFileLinkLabel, &QLabel::linkActivated, this, &DatabaseOpenWidget::browseKeyFile); + connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile())); connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) { bool state = !text.isEmpty(); m_ui->addKeyFileLinkLabel->setVisible(!state); m_ui->selectKeyFileComponent->setVisible(state); }); - connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled); - m_ui->selectKeyFileComponent->setVisible(false); + // Hardware key components toggleHardwareKeyComponent(false); + connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled); QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy(); sp.setRetainSizeWhenHidden(true); @@ -127,13 +115,24 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) m_ui->refreshHardwareKeys->setVisible(false); #endif - // QuickUnlock actions + // QuickUnlock components + m_ui->quickUnlockButton->setFont(largeFont); + m_ui->quickUnlockButton->setIcon( + icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText))); + connect(m_ui->quickUnlockButton, &QPushButton::pressed, this, [this] { openDatabase(); }); connect(m_ui->resetQuickUnlockButton, &QPushButton::pressed, this, [this] { resetQuickUnlock(); }); + connect(m_ui->closeQuickUnlockButton, &QPushButton::pressed, this, [this] { reject(); }); m_ui->resetQuickUnlockButton->setShortcut(Qt::Key_Escape); } -DatabaseOpenWidget::~DatabaseOpenWidget() = default; +DatabaseOpenWidget::~DatabaseOpenWidget() +{ + // Reset quick unlock if we are not remembering it + if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) { + resetQuickUnlock(); + } +} void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state) { @@ -161,7 +160,7 @@ bool DatabaseOpenWidget::event(QEvent* event) auto type = event->type(); if (type == QEvent::Show || type == QEvent::WindowActivate) { - if (isOnQuickUnlockScreen() && (m_db.isNull() || !canPerformQuickUnlock())) { + if (isOnQuickUnlockScreen() && !canPerformQuickUnlock()) { resetQuickUnlock(); } toggleQuickUnlockScreen(); @@ -261,6 +260,7 @@ void DatabaseOpenWidget::load(const QString& filename) } toggleQuickUnlockScreen(); + m_ui->enableQuickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool()); #ifdef WITH_XC_YUBIKEY // Do initial auto-poll @@ -302,16 +302,12 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile) m_ui->editPassword->setText(pw); m_ui->keyFileLineEdit->setText(keyFile); - m_blockQuickUnlock = true; + m_ui->enableQuickUnlockCheckBox->setChecked(false); openDatabase(); } void DatabaseOpenWidget::openDatabase() { - // Cache this variable for future use then reset - bool blockQuickUnlock = m_blockQuickUnlock || isOnQuickUnlockScreen(); - m_blockQuickUnlock = false; - setUserInteractionLock(true); m_ui->editPassword->setShowPassword(false); m_ui->messageWidget->hide(); @@ -353,12 +349,12 @@ void DatabaseOpenWidget::openDatabase() } } - // Save Quick Unlock credentials if available - if (!blockQuickUnlock && isQuickUnlockAvailable()) { + // Save Quick Unlock credentials if available and enabled + if (!isOnQuickUnlockScreen() && isQuickUnlockAvailable() && m_ui->enableQuickUnlockCheckBox->isChecked()) { auto keyData = databaseKey->serialize(); - if (!getQuickUnlock()->setKey(m_db->publicUuid(), keyData) && !getQuickUnlock()->errorString().isEmpty()) { - getMainWindow()->displayTabMessage(getQuickUnlock()->errorString(), - MessageWidget::MessageType::Warning); + auto qu = getQuickUnlock()->interface(); + if (!qu->setKey(m_db->publicUuid(), keyData) && !qu->errorString().isEmpty()) { + getMainWindow()->displayTabMessage(qu->errorString(), MessageWidget::MessageType::Warning); } m_ui->messageWidget->hideMessage(); } @@ -404,13 +400,16 @@ QSharedPointer DatabaseOpenWidget::buildDatabaseKey() { auto databaseKey = QSharedPointer::create(); - if (!m_db.isNull() && canPerformQuickUnlock()) { - // try to retrieve the stored password using Windows Hello + if (canPerformQuickUnlock()) { + // try to retrieve the stored password using quick unlock QByteArray keyData; - if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) { - m_ui->messageWidget->showMessage( - tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()), - MessageWidget::Error); + auto qu = getQuickUnlock()->interface(); + if (!qu->getKey(m_db->publicUuid(), keyData)) { + m_ui->messageWidget->showMessage(tr("Failed to authenticate with Quick Unlock: %1").arg(qu->errorString()), + MessageWidget::Error); + if (!qu->hasKey(m_db->publicUuid())) { + resetQuickUnlock(); + } return {}; } databaseKey->setRawKey(keyData); @@ -600,9 +599,15 @@ void DatabaseOpenWidget::setUserInteractionLock(bool state) m_unlockingDatabase = state; } +bool DatabaseOpenWidget::isQuickUnlockAvailable() const +{ + auto qu = getQuickUnlock()->interface(); + return qu && qu->isAvailable(); +} + bool DatabaseOpenWidget::canPerformQuickUnlock() const { - return !m_db.isNull() && isQuickUnlockAvailable() && getQuickUnlock()->hasKey(m_db->publicUuid()); + return m_db && isQuickUnlockAvailable() && getQuickUnlock()->interface()->hasKey(m_db->publicUuid()); } bool DatabaseOpenWidget::isOnQuickUnlockScreen() const @@ -629,7 +634,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen() void DatabaseOpenWidget::triggerQuickUnlock() { - if (isOnQuickUnlockScreen()) { + if (isOnQuickUnlockScreen() && !unlockingDatabase()) { m_ui->quickUnlockButton->click(); } } @@ -641,11 +646,9 @@ void DatabaseOpenWidget::triggerQuickUnlock() */ void DatabaseOpenWidget::resetQuickUnlock() { - if (!isQuickUnlockAvailable()) { - return; - } - if (!m_db.isNull()) { - getQuickUnlock()->reset(m_db->publicUuid()); + auto qu = getQuickUnlock()->interface(); + if (m_db && qu) { + qu->reset(m_db->publicUuid()); } load(m_filename); } diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index f75e118de..d8382d8c6 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -19,7 +19,6 @@ #ifndef KEEPASSX_DATABASEOPENWIDGET_H #define KEEPASSX_DATABASEOPENWIDGET_H -#include #include #include @@ -45,19 +44,15 @@ class DatabaseOpenWidget : public DialogyWidget public: explicit DatabaseOpenWidget(QWidget* parent = nullptr); ~DatabaseOpenWidget() override; + void load(const QString& filename); QString filename(); + QSharedPointer database(); + void clearForms(); void enterKey(const QString& pw, const QString& keyFile); - QSharedPointer database(); - bool unlockingDatabase(); - - // Quick Unlock helper functions - bool canPerformQuickUnlock() const; - bool isOnQuickUnlockScreen() const; - void toggleQuickUnlockScreen(); void triggerQuickUnlock(); - void resetQuickUnlock(); + bool unlockingDatabase(); signals: void dialogFinished(bool accepted); @@ -69,8 +64,6 @@ protected: const QScopedPointer m_ui; QSharedPointer m_db; - QString m_filename; - bool m_retryUnlockWithEmptyPassword = false; protected slots: virtual void openDatabase(); @@ -81,15 +74,25 @@ private slots: void toggleHardwareKeyComponent(bool state); void pollHardwareKey(bool manualTrigger = false); void hardwareKeyResponse(bool found); + void resetQuickUnlock(); private: + // Quick Unlock helper functions + bool isQuickUnlockAvailable() const; + bool canPerformQuickUnlock() const; + bool isOnQuickUnlockScreen() const; + void toggleQuickUnlockScreen(); + #ifdef WITH_XC_YUBIKEY QPointer m_deviceListener; #endif bool m_pollingHardwareKey = false; bool m_manualHardwareKeyRefresh = false; - bool m_blockQuickUnlock = false; bool m_unlockingDatabase = false; + bool m_retryUnlockWithEmptyPassword = false; + + QString m_filename; + QTimer m_hideTimer; QTimer m_hideNoHardwareKeysFoundTimer; diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 1ef04a528..44857cc23 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -142,7 +142,7 @@ 1 - 0 + 1 @@ -465,6 +465,48 @@ 5 + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::RightToLeft + + + Enable Quick Unlock + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + @@ -511,6 +553,9 @@ + + 0 + @@ -542,17 +587,69 @@ Unlock Database + + + 32 + 32 + + true - - - Cancel + + + Qt::Vertical - + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + 0 + + + + + Reset + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + Close Database + + + + @@ -646,7 +743,6 @@ quickUnlockButton - resetQuickUnlockButton editPassword keyFileLineEdit buttonBrowseFile diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 91aadf839..bb20cdb03 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1916,11 +1916,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event) event->ignore(); return; } - - // Reset quick unlock if we are not remembering it - if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) { - m_databaseOpenWidget->resetQuickUnlock(); - } event->accept(); } diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp index a74b20ead..7d6081bcc 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp @@ -229,7 +229,7 @@ bool DatabaseSettingsWidgetDatabaseKey::saveSettings() m_db->setKey(newKey, true, false, false); - getQuickUnlock()->reset(m_db->publicUuid()); + getQuickUnlock()->interface()->reset(m_db->publicUuid()); emit editFinished(true); if (m_isDirty) { diff --git a/src/quickunlock/PinUnlock.cpp b/src/quickunlock/PinUnlock.cpp new file mode 100644 index 000000000..54d7ef1ee --- /dev/null +++ b/src/quickunlock/PinUnlock.cpp @@ -0,0 +1,171 @@ +/* + * 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 "PinUnlock.h" + +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" +#include "crypto/SymmetricCipher.h" + +#include +#include + +#define MIN_PIN_LENGTH 4 +#define MAX_PIN_LENGTH 8 +#define MAX_PIN_ATTEMPTS 3 + +bool PinUnlock::isAvailable() const +{ + return true; +} + +QString PinUnlock::errorString() const +{ + return m_error; +} + +bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data) +{ + QString pin; + QRegularExpression pinRegex("^\\d+$"); + while (true) { + bool ok = false; + pin = QInputDialog::getText( + nullptr, + QObject::tr("Quick Unlock Pin Entry"), + QObject::tr("Enter a %1 to %2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH), + QLineEdit::Password, + {}, + &ok); + + if (!ok) { + m_error = QObject::tr("Pin setup was canceled. Quick unlock has not been enabled."); + return false; + } + + // Validate pin criteria + if (pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH && pinRegex.match(pin).hasMatch()) { + break; + } + } + + // Hash the pin and use it as the key for the encryption + CryptoHash hash(CryptoHash::Sha256); + hash.addData(pin.toLatin1()); + auto key = hash.result(); + + // Generate a random IV + auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); + + // Encrypt the data using AES-256-CBC + SymmetricCipher cipher; + if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) { + m_error = QObject::tr("Failed to init KeePassXC crypto."); + return false; + } + QByteArray encrypted = data; + if (!cipher.finish(encrypted)) { + m_error = QObject::tr("Failed to encrypt key data."); + return false; + } + + // Prepend the IV to the encrypted data + encrypted.prepend(iv); + // Store the encrypted data and pin attempts + m_encryptedKeys.insert(dbUuid, qMakePair(1, encrypted)); + + return true; +} + +bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data) +{ + data.clear(); + if (!hasKey(dbUuid)) { + m_error = QObject::tr("Failed to get credentials for quick unlock."); + return false; + } + + const auto& pairData = m_encryptedKeys.value(dbUuid); + + // Restrict pin attempts per database + for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) { + bool ok = false; + auto pin = QInputDialog::getText( + nullptr, + QObject::tr("Quick Unlock Pin Entry"), + QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(pinAttempts).arg(MAX_PIN_ATTEMPTS), + QLineEdit::Password, + {}, + &ok); + + if (!ok) { + m_error = QObject::tr("Pin entry was canceled."); + return false; + } + + // Hash the pin and use it as the key for the encryption + CryptoHash hash(CryptoHash::Sha256); + hash.addData(pin.toLatin1()); + auto key = hash.result(); + + // Read the previously used challenge and encrypted data + auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); + const auto& keydata = pairData.second; + auto challenge = keydata.left(ivSize); + auto encrypted = keydata.mid(ivSize); + + // Decrypt the data using the generated key and IV from above + SymmetricCipher cipher; + if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) { + m_error = QObject::tr("Failed to init KeePassXC crypto."); + return false; + } + + // Store the decrypted data into the passed parameter + data = encrypted; + if (cipher.finish(data)) { + // Reset the pin attempts + m_encryptedKeys.insert(dbUuid, qMakePair(1, keydata)); + return true; + } + } + + data.clear(); + m_error = QObject::tr("Maximum pin attempts have been reached."); + reset(dbUuid); + return false; +} + +bool PinUnlock::hasKey(const QUuid& dbUuid) const +{ + return m_encryptedKeys.contains(dbUuid); +} + +bool PinUnlock::canRemember() const +{ + return false; +} + +void PinUnlock::reset(const QUuid& dbUuid) +{ + m_encryptedKeys.remove(dbUuid); +} + +void PinUnlock::reset() +{ + m_encryptedKeys.clear(); +} diff --git a/src/quickunlock/PinUnlock.h b/src/quickunlock/PinUnlock.h new file mode 100644 index 000000000..c285ad8e9 --- /dev/null +++ b/src/quickunlock/PinUnlock.h @@ -0,0 +1,49 @@ +/* + * 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 KEEPASSXC_PINUNLOCK_H +#define KEEPASSXC_PINUNLOCK_H + +#include "QuickUnlockInterface.h" + +#include + +class PinUnlock : public QuickUnlockInterface +{ +public: + PinUnlock() = default; + + bool isAvailable() const override; + QString errorString() const override; + + bool setKey(const QUuid& dbUuid, const QByteArray& key) override; + bool getKey(const QUuid& dbUuid, QByteArray& key) override; + bool hasKey(const QUuid& dbUuid) const override; + + bool canRemember() const override; + + void reset(const QUuid& dbUuid) override; + void reset() override; + +private: + QString m_error; + QHash> m_encryptedKeys; + + Q_DISABLE_COPY(PinUnlock) +}; + +#endif // KEEPASSXC_PINUNLOCK_H diff --git a/src/quickunlock/QuickUnlockInterface.cpp b/src/quickunlock/QuickUnlockInterface.cpp index a04d7a1da..4b46f7797 100644 --- a/src/quickunlock/QuickUnlockInterface.cpp +++ b/src/quickunlock/QuickUnlockInterface.cpp @@ -16,71 +16,63 @@ */ #include "QuickUnlockInterface.h" +#include "PinUnlock.h" + #include #if defined(Q_OS_MACOS) #include "TouchID.h" -#define QUICKUNLOCK_IMPLEMENTATION TouchID #elif defined(Q_CC_MSVC) #include "WindowsHello.h" -#define QUICKUNLOCK_IMPLEMENTATION WindowsHello #elif defined(Q_OS_LINUX) #include "Polkit.h" -#define QUICKUNLOCK_IMPLEMENTATION Polkit -#else -#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock #endif -QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr}; +QuickUnlockManager* g_quickUnlockManager = nullptr; -QuickUnlockInterface* getQuickUnlock() +QuickUnlockManager* getQuickUnlock() { - if (!quickUnlockInstance) { - quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION(); + if (!g_quickUnlockManager) { + g_quickUnlockManager = new QuickUnlockManager(); } - return quickUnlockInstance; + return g_quickUnlockManager; } -bool NoQuickUnlock::isAvailable() const +QuickUnlockManager::QuickUnlockManager() { - return false; + // Create the native interface based on the platform +#if defined(Q_OS_MACOS) + m_nativeInterface.reset(new TouchId()); +#elif defined(Q_CC_MSVC) + m_nativeInterface.reset(new WindowsHello()); +#elif defined(Q_OS_LINUX) + m_nativeInterface.reset(new Polkit()); +#endif + // Always create the fallback interface + m_fallbackInterface.reset(new PinUnlock()); } -QString NoQuickUnlock::errorString() const -{ - return QObject::tr("No Quick Unlock provider is available"); -} - -void NoQuickUnlock::reset() +QuickUnlockManager::~QuickUnlockManager() { } -bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key) +QSharedPointer QuickUnlockManager::interface() const { - Q_UNUSED(dbUuid) - Q_UNUSED(key) - return false; + if (isNativeAvailable()) { + return m_nativeInterface; + } + return m_fallbackInterface; } -bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key) +bool QuickUnlockManager::isNativeAvailable() const { - Q_UNUSED(dbUuid) - Q_UNUSED(key) - return false; + return m_nativeInterface && m_nativeInterface->isAvailable(); } -bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const +bool QuickUnlockManager::isRememberAvailable() const { - Q_UNUSED(dbUuid) - return false; -} - -bool NoQuickUnlock::canRemember() const -{ - return false; -} - -void NoQuickUnlock::reset(const QUuid& dbUuid) -{ - Q_UNUSED(dbUuid) + if (isNativeAvailable()) { + return m_nativeInterface->canRemember(); + } + return m_fallbackInterface->canRemember(); } diff --git a/src/quickunlock/QuickUnlockInterface.h b/src/quickunlock/QuickUnlockInterface.h index 419fffe0a..7908f2e59 100644 --- a/src/quickunlock/QuickUnlockInterface.h +++ b/src/quickunlock/QuickUnlockInterface.h @@ -18,11 +18,12 @@ #ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H #define KEEPASSXC_QUICKUNLOCKINTERFACE_H +#include #include class QuickUnlockInterface { - Q_DISABLE_COPY(QuickUnlockInterface) + Q_DISABLE_COPY_MOVE(QuickUnlockInterface) public: QuickUnlockInterface() = default; @@ -41,22 +42,23 @@ public: virtual void reset() = 0; }; -class NoQuickUnlock : public QuickUnlockInterface +class QuickUnlockManager final { + Q_DISABLE_COPY_MOVE(QuickUnlockManager) + public: - bool isAvailable() const override; - QString errorString() const override; + QuickUnlockManager(); + ~QuickUnlockManager(); - bool setKey(const QUuid& dbUuid, const QByteArray& key) override; - bool getKey(const QUuid& dbUuid, QByteArray& key) override; - bool hasKey(const QUuid& dbUuid) const override; + QSharedPointer interface() const; + bool isNativeAvailable() const; + bool isRememberAvailable() const; - bool canRemember() const override; - - void reset(const QUuid& dbUuid) override; - void reset() override; +private: + QSharedPointer m_nativeInterface; + QSharedPointer m_fallbackInterface; }; -QuickUnlockInterface* getQuickUnlock(); +QuickUnlockManager* getQuickUnlock(); #endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H diff --git a/src/quickunlock/WindowsHello.h b/src/quickunlock/WindowsHello.h index 3032b89a9..67f504bb2 100644 --- a/src/quickunlock/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -20,9 +20,6 @@ #include "QuickUnlockInterface.h" -#include -#include - class WindowsHello : public QuickUnlockInterface { public: @@ -39,10 +36,11 @@ public: void reset(const QUuid& dbUuid) override; void reset() override; - + private: QString m_error; - Q_DISABLE_COPY(WindowsHello); + + Q_DISABLE_COPY(WindowsHello) }; #endif // KEEPASSXC_WINDOWSHELLO_H