Add Pin Quick Unlock option

* Introduce QuickUnlockManager to fall back to pin unlock if OS native options are not available.
This commit is contained in:
Jonathan White 2024-12-01 23:46:56 -05:00
parent 0ee002d963
commit 8e4cba3e9b
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
16 changed files with 532 additions and 221 deletions

View File

@ -586,10 +586,6 @@
<source>Convenience</source> <source>Convenience</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Enable database quick unlock (Touch ID / Windows Hello)</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Lock databases when session is locked or lid is closed</source> <source>Lock databases when session is locked or lid is closed</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -634,6 +630,18 @@
<source>Hide notes in the entry preview panel</source> <source>Hide notes in the entry preview panel</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Quick unlock can only be remembered when using Touch ID or Windows Hello</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enable database quick unlock by default</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remember quick unlock after database is closed (Touch ID / Windows Hello only)</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>AutoType</name> <name>AutoType</name>
@ -1527,10 +1535,6 @@ Backup database located at %2</source>
<source>Unlock Database</source> <source>Unlock Database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Unlock</source> <source>Unlock</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1664,6 +1668,18 @@ 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> <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> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Enable Quick Unlock</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reset</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Database</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DatabaseSettingWidgetMetaData</name> <name>DatabaseSettingWidgetMetaData</name>
@ -8852,46 +8868,10 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Passkeys</source> <source>Passkeys</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>AES initialization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES encrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to store in Linux Keyring</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Polkit returned an error: %1</source> <source>Polkit returned an error: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Could not locate key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not read key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES decrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Polkit authentication agent was available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit authorization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Quick Unlock provider is available</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Failed to init KeePassXC crypto.</source> <source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -9073,6 +9053,58 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Passkey</source> <source>Passkey</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Quick Unlock Pin Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter a %1 to %2 digit pin to use for quick unlock:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pin setup was canceled. Quick unlock has not been enabled.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get credentials for quick unlock.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter quick unlock pin (%1 of %2 attempts):</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pin entry was canceled.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Maximum pin attempts have been reached.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to store key in Linux Keyring. Quick unlock has not been enabled.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not locate key in Linux Keyring.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not read key in Linux Keyring.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Polkit authentication agent was available.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit authorization failed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Windows Hello setup was canceled or failed. Quick unlock has not been enabled.</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>QtIOCompressor</name> <name>QtIOCompressor</name>

View File

@ -205,6 +205,7 @@ set(gui_SOURCES
gui/wizard/NewDatabaseWizardPageEncryption.cpp gui/wizard/NewDatabaseWizardPageEncryption.cpp
gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp
quickunlock/QuickUnlockInterface.cpp quickunlock/QuickUnlockInterface.cpp
quickunlock/PinUnlock.cpp
../share/icons/icons.qrc ../share/icons/icons.qrc
../share/wizard/wizard.qrc) ../share/wizard/wizard.qrc)

View File

@ -130,6 +130,7 @@ public:
Security_EnableCopyOnDoubleClick, Security_EnableCopyOnDoubleClick,
Security_QuickUnlock, Security_QuickUnlock,
Security_QuickUnlockRemember, Security_QuickUnlockRemember,
Security_DatabasePasswordMinimumQuality,
Browser_Enabled, Browser_Enabled,
Browser_ShowNotification, Browser_ShowNotification,

View File

@ -147,10 +147,6 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
m_secUi->lockDatabaseMinimizeCheckBox->setEnabled(!state); 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 // Set Auto-Type shortcut when changed
connect( connect(
m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutChanged, this, [this](auto key, auto modifiers) { 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->hideTotpCheckBox->setChecked(config()->get(Config::Security_HideTotpPreviewPanel).toBool());
m_secUi->hideNotesCheckBox->setChecked(config()->get(Config::Security_HideNotes).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->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->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool());
m_secUi->quickUnlockRememberCheckBox->setToolTip(m_secUi->quickUnlockRememberCheckBox->isEnabled() #ifdef Q_OS_LINUX
? QString() // Remembering quick unlock is not supported on Linux
: tr("Quick unlock cannot be remembered on your device.")); m_secUi->quickUnlockRememberCheckBox->setVisible(false);
#endif
for (const ExtraPage& page : asConst(m_extraPages)) { for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings(); page.loadSettings();
@ -471,10 +462,8 @@ void ApplicationSettingsWidget::saveSettings()
config()->set(Config::Security_HideTotpPreviewPanel, m_secUi->hideTotpCheckBox->isChecked()); config()->set(Config::Security_HideTotpPreviewPanel, m_secUi->hideTotpCheckBox->isChecked());
config()->set(Config::Security_HideNotes, m_secUi->hideNotesCheckBox->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_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked()); config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked());
}
// Security: clear storage if related settings are disabled // Security: clear storage if related settings are disabled
if (!config()->get(Config::RememberLastDatabases).toBool()) { if (!config()->get(Config::RememberLastDatabases).toBool()) {

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>364</width> <width>437</width>
<height>505</height> <height>529</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
@ -168,40 +168,20 @@
<item> <item>
<widget class="QCheckBox" name="quickUnlockCheckBox"> <widget class="QCheckBox" name="quickUnlockCheckBox">
<property name="text"> <property name="text">
<string>Enable database quick unlock (Touch ID / Windows Hello / Polkit)</string> <string>Enable database quick unlock by default</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QCheckBox" name="quickUnlockRememberCheckBox"> <widget class="QCheckBox" name="quickUnlockRememberCheckBox">
<property name="toolTip">
<string>Quick unlock can only be remembered when using Touch ID or Windows Hello</string>
</property>
<property name="text"> <property name="text">
<string>Remember quick unlock after database is closed</string> <string>Remember quick unlock after database is closed (Touch ID / Windows Hello only)</string>
</property> </property>
</widget> </widget>
</item> </item>
</layout>
</item>
<item> <item>
<widget class="QCheckBox" name="lockDatabaseOnScreenLockCheckBox"> <widget class="QCheckBox" name="lockDatabaseOnScreenLockCheckBox">
<property name="text"> <property name="text">

View File

@ -84,9 +84,8 @@ void DatabaseOpenDialog::showEvent(QShowEvent* event)
{ {
QDialog::showEvent(event); QDialog::showEvent(event);
QTimer::singleShot(100, this, [this] { QTimer::singleShot(100, this, [this] {
if (m_view->isOnQuickUnlockScreen() && !m_view->unlockingDatabase()) { // Automatically trigger quick unlock if it's available
m_view->triggerQuickUnlock(); m_view->triggerQuickUnlock();
}
}); });
} }

View File

@ -38,14 +38,6 @@
namespace namespace
{ {
constexpr int clearFormsDelay = 30000; constexpr int clearFormsDelay = 30000;
bool isQuickUnlockAvailable()
{
if (config()->get(Config::Security_QuickUnlock).toBool()) {
return getQuickUnlock()->isAvailable();
}
return false;
}
} // namespace } // namespace
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
@ -68,17 +60,10 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->editPassword->setShowPassword(false); m_ui->editPassword->setShowPassword(false);
}); });
QFont font; QFont largeFont;
font.setPointSize(font.pointSize() + 4); largeFont.setPointSize(largeFont.pointSize() + 4);
font.setBold(true); largeFont.setBold(true);
m_ui->labelHeadline->setFont(font); m_ui->labelHeadline->setFont(largeFont);
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()));
auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok); auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok);
okBtn->setText(tr("Unlock")); 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(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); 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->addKeyFileLinkLabel, &QLabel::linkActivated, this, &DatabaseOpenWidget::browseKeyFile);
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) { connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) {
bool state = !text.isEmpty(); bool state = !text.isEmpty();
m_ui->addKeyFileLinkLabel->setVisible(!state); m_ui->addKeyFileLinkLabel->setVisible(!state);
m_ui->selectKeyFileComponent->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); toggleHardwareKeyComponent(false);
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy(); QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy();
sp.setRetainSizeWhenHidden(true); sp.setRetainSizeWhenHidden(true);
@ -127,13 +115,24 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->refreshHardwareKeys->setVisible(false); m_ui->refreshHardwareKeys->setVisible(false);
#endif #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->quickUnlockButton, &QPushButton::pressed, this, [this] { openDatabase(); });
connect(m_ui->resetQuickUnlockButton, &QPushButton::pressed, this, [this] { resetQuickUnlock(); }); 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); 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) void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state)
{ {
@ -161,7 +160,7 @@ bool DatabaseOpenWidget::event(QEvent* event)
auto type = event->type(); auto type = event->type();
if (type == QEvent::Show || type == QEvent::WindowActivate) { if (type == QEvent::Show || type == QEvent::WindowActivate) {
if (isOnQuickUnlockScreen() && (m_db.isNull() || !canPerformQuickUnlock())) { if (isOnQuickUnlockScreen() && !canPerformQuickUnlock()) {
resetQuickUnlock(); resetQuickUnlock();
} }
toggleQuickUnlockScreen(); toggleQuickUnlockScreen();
@ -261,6 +260,7 @@ void DatabaseOpenWidget::load(const QString& filename)
} }
toggleQuickUnlockScreen(); toggleQuickUnlockScreen();
m_ui->enableQuickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
#ifdef WITH_XC_YUBIKEY #ifdef WITH_XC_YUBIKEY
// Do initial auto-poll // Do initial auto-poll
@ -302,16 +302,12 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
m_ui->editPassword->setText(pw); m_ui->editPassword->setText(pw);
m_ui->keyFileLineEdit->setText(keyFile); m_ui->keyFileLineEdit->setText(keyFile);
m_blockQuickUnlock = true; m_ui->enableQuickUnlockCheckBox->setChecked(false);
openDatabase(); openDatabase();
} }
void DatabaseOpenWidget::openDatabase() void DatabaseOpenWidget::openDatabase()
{ {
// Cache this variable for future use then reset
bool blockQuickUnlock = m_blockQuickUnlock || isOnQuickUnlockScreen();
m_blockQuickUnlock = false;
setUserInteractionLock(true); setUserInteractionLock(true);
m_ui->editPassword->setShowPassword(false); m_ui->editPassword->setShowPassword(false);
m_ui->messageWidget->hide(); m_ui->messageWidget->hide();
@ -353,12 +349,12 @@ void DatabaseOpenWidget::openDatabase()
} }
} }
// Save Quick Unlock credentials if available // Save Quick Unlock credentials if available and enabled
if (!blockQuickUnlock && isQuickUnlockAvailable()) { if (!isOnQuickUnlockScreen() && isQuickUnlockAvailable() && m_ui->enableQuickUnlockCheckBox->isChecked()) {
auto keyData = databaseKey->serialize(); auto keyData = databaseKey->serialize();
if (!getQuickUnlock()->setKey(m_db->publicUuid(), keyData) && !getQuickUnlock()->errorString().isEmpty()) { auto qu = getQuickUnlock()->interface();
getMainWindow()->displayTabMessage(getQuickUnlock()->errorString(), if (!qu->setKey(m_db->publicUuid(), keyData) && !qu->errorString().isEmpty()) {
MessageWidget::MessageType::Warning); getMainWindow()->displayTabMessage(qu->errorString(), MessageWidget::MessageType::Warning);
} }
m_ui->messageWidget->hideMessage(); m_ui->messageWidget->hideMessage();
} }
@ -404,13 +400,16 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
{ {
auto databaseKey = QSharedPointer<CompositeKey>::create(); auto databaseKey = QSharedPointer<CompositeKey>::create();
if (!m_db.isNull() && canPerformQuickUnlock()) { if (canPerformQuickUnlock()) {
// try to retrieve the stored password using Windows Hello // try to retrieve the stored password using quick unlock
QByteArray keyData; QByteArray keyData;
if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) { auto qu = getQuickUnlock()->interface();
m_ui->messageWidget->showMessage( if (!qu->getKey(m_db->publicUuid(), keyData)) {
tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()), m_ui->messageWidget->showMessage(tr("Failed to authenticate with Quick Unlock: %1").arg(qu->errorString()),
MessageWidget::Error); MessageWidget::Error);
if (!qu->hasKey(m_db->publicUuid())) {
resetQuickUnlock();
}
return {}; return {};
} }
databaseKey->setRawKey(keyData); databaseKey->setRawKey(keyData);
@ -600,9 +599,15 @@ void DatabaseOpenWidget::setUserInteractionLock(bool state)
m_unlockingDatabase = state; m_unlockingDatabase = state;
} }
bool DatabaseOpenWidget::isQuickUnlockAvailable() const
{
auto qu = getQuickUnlock()->interface();
return qu && qu->isAvailable();
}
bool DatabaseOpenWidget::canPerformQuickUnlock() const 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 bool DatabaseOpenWidget::isOnQuickUnlockScreen() const
@ -629,7 +634,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen()
void DatabaseOpenWidget::triggerQuickUnlock() void DatabaseOpenWidget::triggerQuickUnlock()
{ {
if (isOnQuickUnlockScreen()) { if (isOnQuickUnlockScreen() && !unlockingDatabase()) {
m_ui->quickUnlockButton->click(); m_ui->quickUnlockButton->click();
} }
} }
@ -641,11 +646,9 @@ void DatabaseOpenWidget::triggerQuickUnlock()
*/ */
void DatabaseOpenWidget::resetQuickUnlock() void DatabaseOpenWidget::resetQuickUnlock()
{ {
if (!isQuickUnlockAvailable()) { auto qu = getQuickUnlock()->interface();
return; if (m_db && qu) {
} qu->reset(m_db->publicUuid());
if (!m_db.isNull()) {
getQuickUnlock()->reset(m_db->publicUuid());
} }
load(m_filename); load(m_filename);
} }

View File

@ -19,7 +19,6 @@
#ifndef KEEPASSX_DATABASEOPENWIDGET_H #ifndef KEEPASSX_DATABASEOPENWIDGET_H
#define KEEPASSX_DATABASEOPENWIDGET_H #define KEEPASSX_DATABASEOPENWIDGET_H
#include <QPointer>
#include <QScopedPointer> #include <QScopedPointer>
#include <QTimer> #include <QTimer>
@ -45,19 +44,15 @@ class DatabaseOpenWidget : public DialogyWidget
public: public:
explicit DatabaseOpenWidget(QWidget* parent = nullptr); explicit DatabaseOpenWidget(QWidget* parent = nullptr);
~DatabaseOpenWidget() override; ~DatabaseOpenWidget() override;
void load(const QString& filename); void load(const QString& filename);
QString filename(); QString filename();
QSharedPointer<Database> database();
void clearForms(); void clearForms();
void enterKey(const QString& pw, const QString& keyFile); void enterKey(const QString& pw, const QString& keyFile);
QSharedPointer<Database> database();
bool unlockingDatabase();
// Quick Unlock helper functions
bool canPerformQuickUnlock() const;
bool isOnQuickUnlockScreen() const;
void toggleQuickUnlockScreen();
void triggerQuickUnlock(); void triggerQuickUnlock();
void resetQuickUnlock(); bool unlockingDatabase();
signals: signals:
void dialogFinished(bool accepted); void dialogFinished(bool accepted);
@ -69,8 +64,6 @@ protected:
const QScopedPointer<Ui::DatabaseOpenWidget> m_ui; const QScopedPointer<Ui::DatabaseOpenWidget> m_ui;
QSharedPointer<Database> m_db; QSharedPointer<Database> m_db;
QString m_filename;
bool m_retryUnlockWithEmptyPassword = false;
protected slots: protected slots:
virtual void openDatabase(); virtual void openDatabase();
@ -81,15 +74,25 @@ private slots:
void toggleHardwareKeyComponent(bool state); void toggleHardwareKeyComponent(bool state);
void pollHardwareKey(bool manualTrigger = false); void pollHardwareKey(bool manualTrigger = false);
void hardwareKeyResponse(bool found); void hardwareKeyResponse(bool found);
void resetQuickUnlock();
private: private:
// Quick Unlock helper functions
bool isQuickUnlockAvailable() const;
bool canPerformQuickUnlock() const;
bool isOnQuickUnlockScreen() const;
void toggleQuickUnlockScreen();
#ifdef WITH_XC_YUBIKEY #ifdef WITH_XC_YUBIKEY
QPointer<DeviceListener> m_deviceListener; QPointer<DeviceListener> m_deviceListener;
#endif #endif
bool m_pollingHardwareKey = false; bool m_pollingHardwareKey = false;
bool m_manualHardwareKeyRefresh = false; bool m_manualHardwareKeyRefresh = false;
bool m_blockQuickUnlock = false;
bool m_unlockingDatabase = false; bool m_unlockingDatabase = false;
bool m_retryUnlockWithEmptyPassword = false;
QString m_filename;
QTimer m_hideTimer; QTimer m_hideTimer;
QTimer m_hideNoHardwareKeysFoundTimer; QTimer m_hideNoHardwareKeysFoundTimer;

View File

@ -142,7 +142,7 @@
<number>1</number> <number>1</number>
</property> </property>
<property name="currentIndex"> <property name="currentIndex">
<number>0</number> <number>1</number>
</property> </property>
<widget class="QWidget" name="mainPage"> <widget class="QWidget" name="mainPage">
<layout class="QVBoxLayout" name="verticalLayout_6"> <layout class="QVBoxLayout" name="verticalLayout_6">
@ -465,6 +465,48 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>5</number> <number>5</number>
</property> </property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="enableQuickUnlockCheckBox">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Enable Quick Unlock</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>8</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item alignment="Qt::AlignRight"> <item alignment="Qt::AlignRight">
<widget class="QDialogButtonBox" name="buttonBox"> <widget class="QDialogButtonBox" name="buttonBox">
<property name="focusPolicy"> <property name="focusPolicy">
@ -511,6 +553,9 @@
</item> </item>
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_5"> <layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<item> <item>
<spacer name="verticalSpacer_7"> <spacer name="verticalSpacer_7">
<property name="orientation"> <property name="orientation">
@ -542,18 +587,70 @@
<property name="text"> <property name="text">
<string>Unlock Database</string> <string>Unlock Database</string>
</property> </property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="default"> <property name="default">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>8</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<item> <item>
<widget class="QPushButton" name="resetQuickUnlockButton"> <widget class="QPushButton" name="resetQuickUnlockButton">
<property name="text"> <property name="text">
<string>Cancel</string> <string>Reset</string>
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>6</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="closeQuickUnlockButton">
<property name="text">
<string>Close Database</string>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<spacer name="verticalSpacer_8"> <spacer name="verticalSpacer_8">
<property name="orientation"> <property name="orientation">
@ -646,7 +743,6 @@
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>quickUnlockButton</tabstop> <tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>editPassword</tabstop> <tabstop>editPassword</tabstop>
<tabstop>keyFileLineEdit</tabstop> <tabstop>keyFileLineEdit</tabstop>
<tabstop>buttonBrowseFile</tabstop> <tabstop>buttonBrowseFile</tabstop>

View File

@ -1916,11 +1916,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event)
event->ignore(); event->ignore();
return; return;
} }
// Reset quick unlock if we are not remembering it
if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) {
m_databaseOpenWidget->resetQuickUnlock();
}
event->accept(); event->accept();
} }

View File

@ -229,7 +229,7 @@ bool DatabaseSettingsWidgetDatabaseKey::saveSettings()
m_db->setKey(newKey, true, false, false); m_db->setKey(newKey, true, false, false);
getQuickUnlock()->reset(m_db->publicUuid()); getQuickUnlock()->interface()->reset(m_db->publicUuid());
emit editFinished(true); emit editFinished(true);
if (m_isDirty) { if (m_isDirty) {

View File

@ -0,0 +1,171 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 "PinUnlock.h"
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include <QInputDialog>
#include <QRegularExpression>
#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();
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 KEEPASSXC_PINUNLOCK_H
#define KEEPASSXC_PINUNLOCK_H
#include "QuickUnlockInterface.h"
#include <QHash>
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<QUuid, QPair<int, QByteArray>> m_encryptedKeys;
Q_DISABLE_COPY(PinUnlock)
};
#endif // KEEPASSXC_PINUNLOCK_H

View File

@ -16,71 +16,63 @@
*/ */
#include "QuickUnlockInterface.h" #include "QuickUnlockInterface.h"
#include "PinUnlock.h"
#include <QObject> #include <QObject>
#if defined(Q_OS_MACOS) #if defined(Q_OS_MACOS)
#include "TouchID.h" #include "TouchID.h"
#define QUICKUNLOCK_IMPLEMENTATION TouchID
#elif defined(Q_CC_MSVC) #elif defined(Q_CC_MSVC)
#include "WindowsHello.h" #include "WindowsHello.h"
#define QUICKUNLOCK_IMPLEMENTATION WindowsHello
#elif defined(Q_OS_LINUX) #elif defined(Q_OS_LINUX)
#include "Polkit.h" #include "Polkit.h"
#define QUICKUNLOCK_IMPLEMENTATION Polkit
#else
#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock
#endif #endif
QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr}; QuickUnlockManager* g_quickUnlockManager = nullptr;
QuickUnlockInterface* getQuickUnlock() QuickUnlockManager* getQuickUnlock()
{ {
if (!quickUnlockInstance) { if (!g_quickUnlockManager) {
quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION(); 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 QuickUnlockManager::~QuickUnlockManager()
{
return QObject::tr("No Quick Unlock provider is available");
}
void NoQuickUnlock::reset()
{ {
} }
bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key) QSharedPointer<QuickUnlockInterface> QuickUnlockManager::interface() const
{ {
Q_UNUSED(dbUuid) if (isNativeAvailable()) {
Q_UNUSED(key) return m_nativeInterface;
return false; }
return m_fallbackInterface;
} }
bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key) bool QuickUnlockManager::isNativeAvailable() const
{ {
Q_UNUSED(dbUuid) return m_nativeInterface && m_nativeInterface->isAvailable();
Q_UNUSED(key)
return false;
} }
bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const bool QuickUnlockManager::isRememberAvailable() const
{ {
Q_UNUSED(dbUuid) if (isNativeAvailable()) {
return false; return m_nativeInterface->canRemember();
} }
return m_fallbackInterface->canRemember();
bool NoQuickUnlock::canRemember() const
{
return false;
}
void NoQuickUnlock::reset(const QUuid& dbUuid)
{
Q_UNUSED(dbUuid)
} }

View File

@ -18,11 +18,12 @@
#ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H #ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H
#define KEEPASSXC_QUICKUNLOCKINTERFACE_H #define KEEPASSXC_QUICKUNLOCKINTERFACE_H
#include <QSharedPointer>
#include <QUuid> #include <QUuid>
class QuickUnlockInterface class QuickUnlockInterface
{ {
Q_DISABLE_COPY(QuickUnlockInterface) Q_DISABLE_COPY_MOVE(QuickUnlockInterface)
public: public:
QuickUnlockInterface() = default; QuickUnlockInterface() = default;
@ -41,22 +42,23 @@ public:
virtual void reset() = 0; virtual void reset() = 0;
}; };
class NoQuickUnlock : public QuickUnlockInterface class QuickUnlockManager final
{ {
Q_DISABLE_COPY_MOVE(QuickUnlockManager)
public: public:
bool isAvailable() const override; QuickUnlockManager();
QString errorString() const override; ~QuickUnlockManager();
bool setKey(const QUuid& dbUuid, const QByteArray& key) override; QSharedPointer<QuickUnlockInterface> interface() const;
bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool isNativeAvailable() const;
bool hasKey(const QUuid& dbUuid) const override; bool isRememberAvailable() const;
bool canRemember() const override; private:
QSharedPointer<QuickUnlockInterface> m_nativeInterface;
void reset(const QUuid& dbUuid) override; QSharedPointer<QuickUnlockInterface> m_fallbackInterface;
void reset() override;
}; };
QuickUnlockInterface* getQuickUnlock(); QuickUnlockManager* getQuickUnlock();
#endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H #endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H

View File

@ -20,9 +20,6 @@
#include "QuickUnlockInterface.h" #include "QuickUnlockInterface.h"
#include <QHash>
#include <QObject>
class WindowsHello : public QuickUnlockInterface class WindowsHello : public QuickUnlockInterface
{ {
public: public:
@ -42,7 +39,8 @@ public:
private: private:
QString m_error; QString m_error;
Q_DISABLE_COPY(WindowsHello);
Q_DISABLE_COPY(WindowsHello)
}; };
#endif // KEEPASSXC_WINDOWSHELLO_H #endif // KEEPASSXC_WINDOWSHELLO_H