diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index f974db170..fd17a022b 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -657,10 +657,6 @@
Convenience
-
- Enable database quick unlock (Touch ID / Windows Hello)
-
-
Lock databases when session is locked or lid is closed
@@ -705,6 +701,14 @@
Hide notes in the entry preview panel
+
+ Enable database quick unlock by default
+
+
+
+ Remember quick unlock after database is closed
+
+
AttachmentWidget
@@ -1654,10 +1658,6 @@ Backup database located at %2
Unlock Database
-
- Cancel
-
-
Unlock
@@ -1735,10 +1735,6 @@ To prevent this error from appearing, you must go to "Database Settings / S
Cannot use database file as key file
-
- authenticate to access the database
-
-
Failed to authenticate with Quick Unlock: %1
@@ -1791,6 +1787,14 @@ Are you sure you want to continue with this file?.
<a href="#" style="text-decoration: underline">I have a key file</a>
+
+ Reset
+
+
+
+ Close Database
+
+
Hardware keys found, but no slots are configured.
@@ -1799,6 +1803,10 @@ Are you sure you want to continue with this file?.
Press ESC again to close this database
+
+ Quick Unlock
+
+
DatabaseSettingWidgetMetaData
@@ -9134,46 +9142,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.
@@ -9182,10 +9154,6 @@ This option is deprecated, use --set-key-file instead.
Failed to encrypt key data.
-
- Failed to get Windows Hello credential.
-
-
Failed to decrypt key data.
@@ -9349,7 +9317,35 @@ This option is deprecated, use --set-key-file instead.
- Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'.
+ Quick Unlock Pin Entry
+
+
+
+ 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.
+
+
+
+ No Polkit authentication agent was available.
+
+
+
+ Polkit authorization failed.
+
+
+
+ Windows Hello setup was canceled or failed. Quick unlock has not been enabled.
@@ -9433,6 +9429,34 @@ This option is deprecated, use --set-key-file instead.
Confirm Replace Entry References
+
+ Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'.
+
+
+
+ Enter a %1–%2 digit pin to use for quick unlock:
+
+
+
+ Failed to derive key using Argon2
+
+
+
+ Too many pin attempts.
+
+
+
+ No key is stored for this database.
+
+
+
+ Failed to obtain session key.
+
+
+
+ Failed to retrieve Windows Hello credential.
+
+
QtIOCompressor
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 6a5eb5d80..68da50d68 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -217,6 +217,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)
@@ -227,40 +228,41 @@ if(APPLE)
gui/osutils/macutils/ScreenLockListenerMac.cpp
gui/osutils/macutils/AppKitImpl.mm
gui/osutils/macutils/AppKit.h
- quickunlock/TouchID.mm)
-
- # TODO: Remove -Wno-error once deprecation warnings have been resolved.
- set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast")
+ quickunlock/TouchID.cpp)
endif()
if(UNIX AND NOT APPLE)
list(APPEND gui_SOURCES
gui/osutils/nixutils/ScreenLockListenerDBus.cpp
gui/osutils/nixutils/NixUtils.cpp)
- if("${CMAKE_SYSTEM}" MATCHES "Linux")
- list(APPEND core_SOURCES
- quickunlock/Polkit.cpp
- quickunlock/PolkitDbusTypes.cpp)
- endif()
if(WITH_XC_X11)
list(APPEND gui_SOURCES
gui/osutils/nixutils/X11Funcs.cpp)
endif()
+
+ # Polkit is only available on Linux systems
+ if("${CMAKE_SYSTEM}" MATCHES "Linux")
+ list(APPEND gui_SOURCES
+ quickunlock/Polkit.cpp
+ quickunlock/PolkitDbusTypes.cpp)
+
+ set_source_files_properties(
+ quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
+ PROPERTIES
+ INCLUDE "quickunlock/PolkitDbusTypes.h"
+ )
+ qt5_add_dbus_interface(gui_SOURCES
+ quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
+ polkit_dbus
+ )
+ endif()
+
+ # dbus support
qt5_add_dbus_adaptor(gui_SOURCES
gui/org.keepassxc.KeePassXC.MainWindow.xml
gui/MainWindow.h
MainWindow)
- set_source_files_properties(
- quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
- PROPERTIES
- INCLUDE "quickunlock/PolkitDbusTypes.h"
- )
- qt5_add_dbus_interface(core_SOURCES
- quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
- polkit_dbus
- )
-
find_library(KEYUTILS_LIBRARIES NAMES keyutils)
if(NOT KEYUTILS_LIBRARIES)
message(FATAL_ERROR "Could not find libkeyutils")
diff --git a/src/core/Config.cpp b/src/core/Config.cpp
index 2c2b0bc57..4a9385617 100644
--- a/src/core/Config.cpp
+++ b/src/core/Config.cpp
@@ -155,7 +155,7 @@ static const QHash configStrings = {
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}},
- {Config::Security_DatabasePasswordMinimumQuality, {QS("Security/DatabasePasswordMinimumQuality"), Local, 0}},
+ {Config::Security_QuickUnlockRemember, {QS("Security/QuickUnlockRemember"), Local, true}},
// Browser
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},
diff --git a/src/core/Config.h b/src/core/Config.h
index 8f54f9c01..7652caecc 100644
--- a/src/core/Config.h
+++ b/src/core/Config.h
@@ -136,6 +136,7 @@ public:
Security_NoConfirmMoveEntryToRecycleBin,
Security_EnableCopyOnDoubleClick,
Security_QuickUnlock,
+ Security_QuickUnlockRemember,
Security_DatabasePasswordMinimumQuality,
Browser_Enabled,
diff --git a/src/crypto/kdf/Argon2Kdf.cpp b/src/crypto/kdf/Argon2Kdf.cpp
index f7b028ffe..233463d57 100644
--- a/src/crypto/kdf/Argon2Kdf.cpp
+++ b/src/crypto/kdf/Argon2Kdf.cpp
@@ -163,6 +163,13 @@ QVariantMap Argon2Kdf::writeParameters()
bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const
{
+ // This is a programming error and will result in broken encryption
+ Q_ASSERT(*raw != *result);
+ if (*raw == *result) {
+ qWarning("Argon2Kdf: Input and output buffers must not be the same.");
+ return false;
+ }
+
result.clear();
result.resize(32);
// Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length
diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp
index 9f24bbc85..3f1f91cad 100644
--- a/src/gui/ApplicationSettingsWidget.cpp
+++ b/src/gui/ApplicationSettingsWidget.cpp
@@ -348,8 +348,15 @@ 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->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool());
+#ifdef Q_OS_LINUX
+ // Remembering quick unlock is not supported on Linux
+ m_secUi->quickUnlockRememberCheckBox->setVisible(false);
+#else
+ // Only show this option if Touch ID or Windows Hello are available for use
+ m_secUi->quickUnlockRememberCheckBox->setVisible(getQuickUnlock()->isNativeAvailable());
+#endif
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings();
@@ -469,9 +476,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_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/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui
index 42ce4acc1..01472e6ba 100644
--- a/src/gui/ApplicationSettingsWidgetGeneral.ui
+++ b/src/gui/ApplicationSettingsWidgetGeneral.ui
@@ -148,7 +148,7 @@
40
- 20
+ 0
@@ -174,7 +174,7 @@
30
- 20
+ 0
@@ -210,7 +210,7 @@
30
- 20
+ 0
@@ -250,7 +250,7 @@
30
- 20
+ 0
@@ -315,7 +315,7 @@
40
- 20
+ 0
@@ -483,7 +483,7 @@
30
- 20
+ 0
@@ -516,7 +516,7 @@
40
- 20
+ 0
@@ -648,7 +648,7 @@
30
- 20
+ 0
@@ -681,7 +681,7 @@
30
- 20
+ 0
@@ -743,7 +743,7 @@
40
- 20
+ 0
@@ -971,7 +971,7 @@
30
- 20
+ 0
@@ -1019,7 +1019,7 @@
40
- 20
+ 0
@@ -1076,7 +1076,7 @@
30
- 20
+ 0
diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui
index c4cb0e01f..56a6f7343 100644
--- a/src/gui/ApplicationSettingsWidgetSecurity.ui
+++ b/src/gui/ApplicationSettingsWidgetSecurity.ui
@@ -6,8 +6,8 @@
0
0
- 364
- 505
+ 437
+ 529
@@ -138,7 +138,7 @@
40
- 20
+ 0
@@ -168,7 +168,14 @@
-
- Enable database quick unlock (Touch ID / Windows Hello)
+ Enable database quick unlock by default
+
+
+
+ -
+
+
+ Remember quick unlock after database is closed
diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp
index 881db4087..70ede8986 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 7cf5dcb15..eef4c2899 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)
{
@@ -189,7 +188,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();
@@ -294,6 +293,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
@@ -335,16 +335,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();
@@ -386,10 +382,13 @@ 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();
- getQuickUnlock()->setKey(m_db->publicUuid(), keyData);
+ 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();
}
@@ -434,13 +433,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);
@@ -627,9 +629,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
@@ -656,7 +664,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen()
void DatabaseOpenWidget::triggerQuickUnlock()
{
- if (isOnQuickUnlockScreen()) {
+ if (isOnQuickUnlockScreen() && !unlockingDatabase()) {
m_ui->quickUnlockButton->click();
}
}
@@ -668,11 +676,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 d730634b3..70046ecd4 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
@@ -46,21 +45,17 @@ 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();
+ void triggerQuickUnlock();
bool unlockingDatabase();
void showMessage(const QString& text, MessageWidget::MessageType type, int autoHideTimeout);
- // Quick Unlock helper functions
- bool canPerformQuickUnlock() const;
- bool isOnQuickUnlockScreen() const;
- void toggleQuickUnlockScreen();
- void triggerQuickUnlock();
- void resetQuickUnlock();
-
signals:
void dialogFinished(bool accepted);
@@ -85,14 +80,20 @@ private slots:
void closeDatabase();
void pollHardwareKey(bool manualTrigger = false, int delay = 0);
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_triedToQuit = false;
QTimer m_hideTimer;
diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui
index 1ef04a528..e479bcac3 100644
--- a/src/gui/DatabaseOpenWidget.ui
+++ b/src/gui/DatabaseOpenWidget.ui
@@ -180,7 +180,7 @@
0
- 10
+ 0
-
@@ -192,7 +192,7 @@
-
- 2
+ 0
-
@@ -250,13 +250,13 @@
0
- 0
+ 10
0
- 10
+ 15
-
@@ -399,7 +399,7 @@
40
- 20
+ 0
@@ -465,6 +465,48 @@
5
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Qt::RightToLeft
+
+
+ Quick Unlock
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 8
+ 20
+
+
+
+
-
@@ -511,6 +553,9 @@
-
+
+ 0
+
-
@@ -542,17 +587,81 @@
Unlock Database
+
+
+ 32
+ 32
+
+
true
-
-
-
- Cancel
+
+
+ Qt::Vertical
-
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 4
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 20
+
+
+
+ Reset
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 4
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 20
+
+
+
+ Close Database
+
+
+
+
-
@@ -645,8 +754,6 @@
- quickUnlockButton
- resetQuickUnlockButton
editPassword
keyFileLineEdit
buttonBrowseFile
@@ -654,7 +761,11 @@
hardwareKeyCombo
refreshHardwareKeys
addKeyFileLinkLabel
+ enableQuickUnlockCheckBox
buttonBox
+ quickUnlockButton
+ resetQuickUnlockButton
+ closeQuickUnlockButton
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index 2afde49fe..d8290705e 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -1978,8 +1978,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event)
event->ignore();
return;
}
-
- 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/gui/osutils/OSUtilsBase.h b/src/gui/osutils/OSUtilsBase.h
index 11d739fde..b43278840 100644
--- a/src/gui/osutils/OSUtilsBase.h
+++ b/src/gui/osutils/OSUtilsBase.h
@@ -72,6 +72,14 @@ public:
virtual bool canPreventScreenCapture() const = 0;
virtual bool setPreventScreenCapture(QWindow* window, bool allow) const;
+ /**
+ * Platform specific secrets storage/handling
+ */
+ virtual bool saveSecret(const QString& key, const QByteArray& secretData) const = 0;
+ virtual bool getSecret(const QString& key, QByteArray& secretData) const = 0;
+ virtual bool removeSecret(const QString& key) const = 0;
+ virtual bool removeAllSecrets() const = 0;
+
signals:
void globalShortcutTriggered(const QString& name, const QString& search = {});
diff --git a/src/gui/osutils/macutils/AppKitImpl.mm b/src/gui/osutils/macutils/AppKitImpl.mm
index 43b3f3723..19645dd29 100644
--- a/src/gui/osutils/macutils/AppKitImpl.mm
+++ b/src/gui/osutils/macutils/AppKitImpl.mm
@@ -17,14 +17,22 @@
*/
#import "AppKitImpl.h"
+#import "MacUtils.h"
+
#import
#import
#import
#import
+#import
+#import
+#import
+#import
#if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3
#import
#endif
+#include "config-keepassx.h"
+
@implementation AppKitImpl
- (id) initWithObject:(AppKit*)appkit
@@ -340,3 +348,222 @@ void AppKit::configureWindowAndHelpMenus(QMainWindow* window, QMenu* helpMenu)
{
[static_cast(self) configureWindowAndHelpMenus:window helpMenu:helpMenu];
}
+
+// Common prefix for saved secrets
+static const auto s_touchIdKeyPrefix = QStringLiteral("KeepassXC_Keys_");
+
+// Convert macOS error codes to strings
+inline std::string StatusToErrorMessage(OSStatus status)
+{
+ CFStringRef text = SecCopyErrorMessageString(status, NULL);
+ if (!text) {
+ return std::to_string(status);
+ }
+
+ auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
+ std::string result;
+ if (msg) {
+ result = msg;
+ }
+ CFRelease(text);
+ return result;
+}
+
+// Report status errors if not successful
+inline void LogStatusError(const char *message, OSStatus status)
+{
+ if (status) {
+ std::string msg = StatusToErrorMessage(status);
+ qWarning("%s: %s", message, msg.c_str());
+ }
+}
+
+// Create an access control object to govern use of the saved secret
+SecAccessControlRef createAccessControl(bool useTouchId)
+{
+ // We need both runtime and compile time checks here to solve the following problems:
+ // - Not all flags are available in all OS versions, so we have to check it at compile time
+ // - Requesting Biometry/TouchID/DevicePassword when no fingerprint sensor is available will result in runtime error
+ SecAccessControlCreateFlags accessControlFlags = 0;
+
+ // When TouchID is not enrolled and the flag is set, the method call fails with an error.
+ // We still want to set this flag if TouchID is enrolled but temporarily unavailable due to closed lid
+ //
+ // Sometimes, the enrolled-check does not work, LAErrorBiometryNotAvailable is returned instead of LAErrorBiometryNotEnrolled.
+ // To fallback gracefully, we have to try to save the key a second time without this flag.
+
+ if (useTouchId) {
+#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
+ // This is the non-deprecated and preferred flag
+ accessControlFlags = kSecAccessControlBiometryCurrentSet;
+#elif XC_COMPILER_SUPPORT(TOUCH_ID)
+ accessControlFlags = kSecAccessControlTouchIDCurrentSet;
+#endif
+ }
+
+ // Add support for watch authentication if available
+#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
+ accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
+#endif
+
+ // Check if password fallback is possible and add that as an option
+#if XC_COMPILER_SUPPORT(TOUCH_ID)
+ if (macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback)) {
+ accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode;
+ }
+#endif
+
+ CFErrorRef error = nullptr;
+ auto sacObject = SecAccessControlCreateWithFlags(
+ kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
+
+ if (!sacObject || error) {
+ auto e = static_cast(error);
+ qWarning("MacUtils::saveSecret - Error creating security flags: %s", e.localizedDescription.UTF8String);
+ return nullptr;
+ }
+ return sacObject;
+}
+
+bool MacUtils::saveSecret(const QString& key, const QByteArray& secretData) const
+{
+ const auto keyName = s_touchIdKeyPrefix + key;
+
+ // Delete any existing entry since macOS does not allow overwrite
+ if (!removeSecret(key)) {
+ qWarning("MacUtils::saveSecret - Failed to remove existing secret for key '%s'", qPrintable(key));
+ }
+
+ // Add new entry
+ auto keyBase64 = secretData.toBase64();
+ auto keyValueData = CFDataCreateWithBytesNoCopy(
+ kCFAllocatorDefault, reinterpret_cast(keyBase64.data()),
+ keyBase64.length(), kCFAllocatorDefault);
+
+ auto attributes = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
+ CFDictionarySetValue(attributes, kSecAttrAccount, static_cast(keyName.toNSString()));
+ CFDictionarySetValue(attributes, kSecValueData, keyValueData);
+ CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
+ CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
+ // First, attempt with TouchID enabled
+ CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(true));
+
+ auto status = SecItemAdd(attributes, nullptr);
+ if (status != errSecSuccess) {
+ qDebug("MacUtils::saveSecret - Failed to save secret with TouchID enabled");
+ // Try again without TouchID enabled
+ CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(false));
+ status = SecItemAdd(attributes, nullptr);
+ if (status != errSecSuccess) {
+ qWarning("MacUtils::saveSecret - Failed to save secret to keystore");
+ }
+ }
+
+ CFRelease(keyValueData);
+ CFRelease(attributes);
+
+ return status == errSecSuccess;
+}
+
+bool MacUtils::getSecret(const QString& key, QByteArray& secretData) const
+{
+ const auto keyName = s_touchIdKeyPrefix + key;
+ secretData.clear();
+
+ auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
+ CFDictionarySetValue(query, kSecAttrAccount, static_cast(keyName.toNSString()));
+ CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
+
+ CFTypeRef dataTypeRef = nullptr;
+ auto status = SecItemCopyMatching(query, &dataTypeRef);
+ CFRelease(query);
+
+ if (status == errSecUserCanceled) {
+ // user canceled the authentication, return true with empty key
+ return true;
+ } else if (status != errSecSuccess || !dataTypeRef) {
+ // TODO: Log failure
+ return false;
+ }
+
+ auto valueData = static_cast(dataTypeRef);
+ secretData = QByteArray::fromBase64(QByteArray(reinterpret_cast(CFDataGetBytePtr(valueData)),
+ CFDataGetLength(valueData)));
+ CFRelease(dataTypeRef);
+
+ return !secretData.isEmpty();
+}
+
+bool MacUtils::removeSecret(const QString& key) const
+{
+ const auto keyName = s_touchIdKeyPrefix + key;
+ auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
+ CFDictionarySetValue(query, kSecAttrAccount, static_cast(keyName.toNSString()));
+ CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
+ // TODO: Log failure to delete?
+ SecItemDelete(query);
+ CFRelease(query);
+ return true;
+}
+
+bool MacUtils::removeAllSecrets() const
+{
+ auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
+ CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue);
+ CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll);
+
+ CFTypeRef result = nullptr;
+ auto status = SecItemCopyMatching(query, &result);
+ if (status == errSecSuccess && result) {
+ for (NSDictionary* item in static_cast(result)) {
+ NSString* account = item[static_cast(kSecAttrAccount)];
+ if (account && [account hasPrefix:s_touchIdKeyPrefix.toNSString()]) {
+ auto delQuery = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ CFDictionarySetValue(delQuery, kSecClass, kSecClassGenericPassword);
+ CFDictionarySetValue(delQuery, kSecAttrAccount, static_cast(account));
+ // TODO: Log failure to delete?
+ SecItemDelete(delQuery);
+ CFRelease(delQuery);
+ }
+ }
+ CFRelease(result);
+ }
+ CFRelease(query);
+ return true;
+}
+
+bool MacUtils::isAuthPolicyAvailable(AuthPolicy policy) const
+{
+ LAPolicy policyCode;
+ switch (policy) {
+ case AuthPolicy::TouchId:
+ policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
+ break;
+ case AuthPolicy::Watch:
+ policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
+ break;
+ case AuthPolicy::PasswordFallback:
+ policyCode = LAPolicyDeviceOwnerAuthentication;
+ break;
+ default:
+ return false;
+ }
+
+ @try {
+ LAContext *context = [[LAContext alloc] init];
+ NSError *error = nil;
+ bool available = [context canEvaluatePolicy:policyCode error:&error];
+ [context release];
+ if (error) {
+ qDebug("MacUtils::isPolicyAvailable - Policy not available: %s", error.localizedDescription.UTF8String);
+ }
+ return available;
+ } @catch (NSException *exception) {
+ qWarning("MacUtils::isPolicyAvailable - Exception occurred: %s", exception.reason.UTF8String);
+ return false;
+ }
+}
diff --git a/src/gui/osutils/macutils/MacUtils.h b/src/gui/osutils/macutils/MacUtils.h
index 403419301..ec60db4a7 100644
--- a/src/gui/osutils/macutils/MacUtils.h
+++ b/src/gui/osutils/macutils/MacUtils.h
@@ -68,6 +68,21 @@ public:
bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
+ // Key management API (TouchID)
+ bool saveSecret(const QString& key, const QByteArray& secretData) const override;
+ bool getSecret(const QString& key, QByteArray& secretData) const override;
+ bool removeSecret(const QString& key) const override;
+ bool removeAllSecrets() const override;
+
+ enum class AuthPolicy
+ {
+ TouchId,
+ Watch,
+ PasswordFallback
+ };
+
+ bool isAuthPolicyAvailable(AuthPolicy policy) const;
+
signals:
void userSwitched();
diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp
index 225d6a05e..c1347161e 100644
--- a/src/gui/osutils/nixutils/NixUtils.cpp
+++ b/src/gui/osutils/nixutils/NixUtils.cpp
@@ -30,6 +30,11 @@
#include
#include
#include
+
+extern "C" {
+#include
+}
+
#ifdef WITH_XC_X11
#include
@@ -411,3 +416,74 @@ quint64 NixUtils::getProcessStartTime() const
qDebug() << "nixutils: failed to find ')' in " << processStatPath;
return 0;
}
+
+namespace
+{
+ key_serial_t getKeyring()
+ {
+ auto keyring = keyctl_get_persistent(-1, KEY_SPEC_PROCESS_KEYRING);
+ if (keyring == -1) {
+ // Return the non-persistent keyring as a fallback
+ qWarning("nixutils: failed to get persistent keyring: %s", strerror(errno));
+ keyring = KEY_SPEC_PROCESS_KEYRING;
+ }
+ return keyring;
+ }
+} // namespace
+
+bool NixUtils::saveSecret(const QString& key, const QByteArray& secretData) const
+{
+ auto keyserial =
+ add_key("user", key.toStdString().c_str(), secretData.constData(), secretData.size(), getKeyring());
+ if (keyserial < 0) {
+ qWarning("nixutils: failed to save secret: %s", strerror(errno));
+ return false;
+ }
+ // Only allow this process to read/write this key
+ keyctl_setperm(keyserial, KEY_POS_ALL);
+
+ return true;
+}
+
+bool NixUtils::getSecret(const QString& key, QByteArray& secretData) const
+{
+ secretData.clear();
+
+ auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
+ if (keyserial < 0) {
+ qWarning("nixutils: failed to find secret: %s", strerror(errno));
+ return false;
+ }
+
+ secretData.resize(512);
+ auto size = keyctl_read(keyserial, secretData.data(), secretData.size());
+ if (size == -1) {
+ qWarning("nixutils: failed to read secret: %s", strerror(errno));
+ return false;
+ }
+
+ secretData.resize(size);
+ return true;
+}
+
+bool NixUtils::removeSecret(const QString& key) const
+{
+ auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
+ if (keyserial < 0) {
+ qWarning("nixutils: failed to find secret: %s", strerror(errno));
+ return false;
+ }
+
+ if (keyctl_unlink(keyserial, getKeyring()) < 0) {
+ qWarning("nixutils: failed to remove secret: %s", strerror(errno));
+ return false;
+ }
+
+ return true;
+}
+
+bool NixUtils::removeAllSecrets() const
+{
+ // NixUtils does not support clearing all keys
+ return false;
+}
diff --git a/src/gui/osutils/nixutils/NixUtils.h b/src/gui/osutils/nixutils/NixUtils.h
index 9be835ff9..bbf4713e1 100644
--- a/src/gui/osutils/nixutils/NixUtils.h
+++ b/src/gui/osutils/nixutils/NixUtils.h
@@ -52,6 +52,11 @@ public:
quint64 getProcessStartTime() const;
+ bool saveSecret(const QString& key, const QByteArray& secretData) const override;
+ bool getSecret(const QString& key, QByteArray& secretData) const override;
+ bool removeSecret(const QString& key) const override;
+ bool removeAllSecrets() const override;
+
private slots:
void handleColorSchemeRead(QDBusVariant value);
void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value);
diff --git a/src/gui/osutils/winutils/WinUtils.cpp b/src/gui/osutils/winutils/WinUtils.cpp
index a15976932..6d242963c 100644
--- a/src/gui/osutils/winutils/WinUtils.cpp
+++ b/src/gui/osutils/winutils/WinUtils.cpp
@@ -20,11 +20,24 @@
#include
#include
#include
+#include
#include
#include
+#include
+#include
+#include
#undef MessageBox
+using namespace winrt;
+using namespace Windows::Foundation::Collections;
+using namespace Windows::Security::Credentials;
+
+namespace
+{
+ const std::wstring s_winKeyStoreName{L"keepassxc"};
+}
+
QPointer WinUtils::m_instance = nullptr;
WinUtils* WinUtils::instance()
@@ -361,3 +374,59 @@ DWORD WinUtils::qtToNativeModifiers(Qt::KeyboardModifiers modifiers)
return nativeModifiers;
}
+
+bool WinUtils::saveSecret(const QString& key, const QByteArray& secretData) const
+{
+ try {
+ auto vault = PasswordVault();
+ vault.Add({s_winKeyStoreName,
+ winrt::hstring(key.toStdWString()),
+ winrt::to_hstring(secretData.toBase64().toStdString())});
+ return true;
+ } catch (winrt::hresult_error const&) {
+ qWarning("WinUtils - Failed to add key to password vault");
+ return false;
+ }
+}
+
+bool WinUtils::getSecret(const QString& key, QByteArray& secretData) const
+{
+ secretData.clear();
+ try {
+ auto vault = PasswordVault();
+ auto credential = vault.Retrieve(s_winKeyStoreName, winrt::hstring(key.toStdWString()));
+ secretData = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password())));
+ } catch (winrt::hresult_error const&) {
+ qWarning("WinUtils - Failed to retrieve key from password vault");
+ return false;
+ }
+ return !secretData.isEmpty();
+}
+
+bool WinUtils::removeSecret(const QString& key) const
+{
+ try {
+ auto vault = PasswordVault();
+ vault.Remove({s_winKeyStoreName, winrt::hstring(key.toStdWString()), L"nodata"});
+ return true;
+ } catch (winrt::hresult_error const&) {
+ qWarning("WinUtils - Failed to clear key from password vault");
+ return false;
+ }
+}
+
+bool WinUtils::removeAllSecrets() const
+{
+ auto vault = PasswordVault();
+ auto credentials = vault.FindAllByResource(s_winKeyStoreName);
+ bool allSuccess = true;
+ for (const auto& credential : credentials) {
+ try {
+ vault.Remove(credential);
+ } catch (winrt::hresult_error const&) {
+ qWarning("WinUtils - Failed to clear key from password vault");
+ allSuccess = false;
+ }
+ }
+ return allSuccess;
+}
diff --git a/src/gui/osutils/winutils/WinUtils.h b/src/gui/osutils/winutils/WinUtils.h
index 9278c9d60..680d19196 100644
--- a/src/gui/osutils/winutils/WinUtils.h
+++ b/src/gui/osutils/winutils/WinUtils.h
@@ -61,6 +61,11 @@ public:
bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
+ bool saveSecret(const QString& key, const QByteArray& secretData) const override;
+ bool getSecret(const QString& key, QByteArray& secretData) const override;
+ bool removeSecret(const QString& key) const override;
+ bool removeAllSecrets() const override;
+
protected:
explicit WinUtils(QObject* parent = nullptr);
~WinUtils() override = default;
diff --git a/src/quickunlock/PinUnlock.cpp b/src/quickunlock/PinUnlock.cpp
new file mode 100644
index 000000000..d4ca6ca8d
--- /dev/null
+++ b/src/quickunlock/PinUnlock.cpp
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2025 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 "crypto/kdf/Argon2Kdf.h"
+#include "gui/osutils/OSUtils.h"
+
+#include
+#include
+
+const int PinUnlock::MIN_PIN_LENGTH = 6;
+const int PinUnlock::MAX_PIN_LENGTH = 10;
+const int PinUnlock::MAX_PIN_ATTEMPTS = 3;
+
+bool PinUnlock::isAvailable() const
+{
+ return true;
+}
+
+bool PinUnlock::promptPin(int attempt, QByteArray& sessionKey)
+{
+ QString pin;
+
+ if (attempt == 0) {
+ // Loop until a valid pin has been entered or canceled
+ QRegularExpression pinRegex("^\\d+$");
+ while (true) {
+ bool ok = false;
+ pin = QInputDialog::getText(
+ nullptr,
+ QObject::tr("Quick Unlock Pin Entry"),
+ QObject::tr("Enter a %1–%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()) {
+ // Pin is valid, move to hashing
+ break;
+ }
+ }
+ } else {
+ bool ok = false;
+ pin = QInputDialog::getText(
+ nullptr,
+ QObject::tr("Quick Unlock Pin Entry"),
+ QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(attempt).arg(MAX_PIN_ATTEMPTS),
+ QLineEdit::Password,
+ {},
+ &ok);
+
+ if (!ok) {
+ // User canceled the pin entry dialog, record pin attempts
+ m_error = QObject::tr("Pin entry was canceled.");
+ return false;
+ }
+ }
+
+ // Hash the pin then run it through Argon2 to derive the encryption key
+ sessionKey.fill('\0', 32);
+ Argon2Kdf kdf(Argon2Kdf::Type::Argon2id);
+ CryptoHash hash(CryptoHash::Sha256);
+ hash.addData(pin.toLatin1());
+ if (!kdf.transform(hash.result(), sessionKey)) {
+ m_error = QObject::tr("Failed to derive key using Argon2");
+ return false;
+ }
+
+ return true;
+}
+
+bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data)
+{
+ QByteArray key;
+ if (!promptPin(0, key)) {
+ // Pin entry was canceled or failed, error set by promptPin
+ return false;
+ }
+
+ // Generate a random IV
+ const auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
+
+ // Encrypt the data using AES-256-GCM
+ 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;
+ }
+
+ // Store the encrypted data
+ saveKey(dbUuid, encrypted.prepend(iv));
+ return true;
+}
+
+bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data)
+{
+ data.clear();
+ bool hasSecret = m_encryptedKeys.contains(dbUuid);
+ if (!hasSecret) {
+ // Check if the OS has a secret stored for this database UUID
+ QByteArray tmp;
+ if (osUtils->getSecret(dbUuid.toString(), tmp)) {
+ // Cache the secret in memory
+ m_encryptedKeys.insert(dbUuid, qMakePair(1, tmp));
+ } else {
+ m_error = QObject::tr("Failed to get credentials for quick unlock.");
+ return false;
+ }
+ }
+
+ // Restrict pin attempts per database
+ const auto& pairData = m_encryptedKeys.value(dbUuid);
+ for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
+ QByteArray key;
+ if (!promptPin(pinAttempts, key)) {
+ // Pin entry was canceled or failed, error set by promptPin
+ m_encryptedKeys.insert(dbUuid, qMakePair(pinAttempts, pairData.second));
+ return false;
+ }
+
+ // Read the previously used challenge and encrypted data
+ const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
+ const auto& iv = pairData.second.left(ivSize);
+
+ // Decrypt the data using the generated key and IV from above
+ SymmetricCipher cipher;
+ if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
+ m_error = QObject::tr("Failed to init KeePassXC crypto.");
+ return false;
+ }
+
+ // Attempt to decrypt the key data
+ data = pairData.second.mid(ivSize);
+ if (cipher.finish(data)) {
+ // Decryption succeeded, reset the pin attempts
+ m_encryptedKeys.insert(dbUuid, qMakePair(1, pairData.second));
+ return true;
+ }
+ }
+
+ data.clear();
+ m_error = QObject::tr("Too many pin attempts.");
+ reset(dbUuid);
+ return false;
+}
+
+void PinUnlock::saveKey(const QUuid& dbUuid, const QByteArray& data)
+{
+ // Save the key to the OS secret store
+ if (!osUtils->saveSecret(dbUuid.toString(), data)) {
+ qWarning("PinUnlock - Failed to save quick unlock credentials.");
+ }
+ // Store the encrypted key in memory
+ m_encryptedKeys.insert(dbUuid, qMakePair(1, data));
+}
+
+bool PinUnlock::hasKey(const QUuid& dbUuid) const
+{
+ bool hasSecret = m_encryptedKeys.contains(dbUuid);
+ if (!hasSecret) {
+ // Check if the OS has a secret stored for this database UUID
+ QByteArray tmp;
+ hasSecret = osUtils->getSecret(dbUuid.toString(), tmp);
+ }
+ return hasSecret;
+}
+
+void PinUnlock::reset(const QUuid& dbUuid)
+{
+ m_encryptedKeys.remove(dbUuid);
+ osUtils->removeSecret(dbUuid.toString());
+}
+
+void PinUnlock::reset()
+{
+ m_encryptedKeys.clear();
+ osUtils->removeAllSecrets();
+}
diff --git a/src/quickunlock/PinUnlock.h b/src/quickunlock/PinUnlock.h
new file mode 100644
index 000000000..081b74405
--- /dev/null
+++ b/src/quickunlock/PinUnlock.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2025 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;
+
+ bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
+ bool getKey(const QUuid& dbUuid, QByteArray& key) override;
+ bool hasKey(const QUuid& dbUuid) const override;
+
+ void reset(const QUuid& dbUuid) override;
+ void reset() override;
+
+ static const int MIN_PIN_LENGTH;
+ static const int MAX_PIN_LENGTH;
+ static const int MAX_PIN_ATTEMPTS;
+
+protected:
+ bool promptPin(int attempt, QByteArray& sessionKey);
+
+private:
+ void saveKey(const QUuid& dbUuid, const QByteArray& key);
+
+ QHash> m_encryptedKeys;
+
+ Q_DISABLE_COPY(PinUnlock)
+};
+
+#endif // KEEPASSXC_PINUNLOCK_H
diff --git a/src/quickunlock/Polkit.cpp b/src/quickunlock/Polkit.cpp
index d73a7c71b..d22f785b2 100644
--- a/src/quickunlock/Polkit.cpp
+++ b/src/quickunlock/Polkit.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 KeePassXC Team
+ * Copyright (C) 2025 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
@@ -23,8 +23,8 @@
#include "gui/osutils/nixutils/NixUtils.h"
#include
-#include
#include
+
#include
#include
@@ -35,19 +35,11 @@ extern "C" {
const QString polkit_service = "org.freedesktop.PolicyKit1";
const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority";
-namespace
-{
- QString getKeyName(const QUuid& dbUuid)
- {
- static const QString keyPrefix = "keepassxc_polkit_keys_";
- return keyPrefix + dbUuid.toString();
- }
-} // namespace
-
Polkit::Polkit()
{
PolkitSubject::registerMetaType();
PolkitAuthorizationResults::registerMetaType();
+ PolkitActionDescription::registerMetaType();
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden
through an environment variable to return an alternative bus path. This bus could have an application
@@ -61,18 +53,34 @@ Polkit::Polkit()
m_available = bus.isConnected();
if (!m_available) {
- qDebug() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
+ qWarning() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
return;
}
m_available = bus.interface()->isServiceRegistered(polkit_service);
if (!m_available) {
- qDebug() << "polkit: Polkit is not registered on dbus";
+ qWarning() << "polkit: Polkit is not registered on dbus";
return;
}
+ // Initiate the Polkit dbus interface
m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus));
+
+ // Reset available state and check Polkit registered actions for KeePassXC
+ m_available = false;
+ auto kpxcAction = QStringLiteral("org.keepassxc.KeePassXC.unlockDatabase");
+ auto actions = m_polkit->EnumerateActions("");
+ for (const auto& action : actions.value()) {
+ if (action.actionId == kpxcAction) {
+ m_available = true;
+ break;
+ }
+ }
+
+ if (!m_available) {
+ qWarning() << "polkit: KeePassXC Polkit action is not installed";
+ }
}
Polkit::~Polkit()
@@ -81,7 +89,8 @@ Polkit::~Polkit()
void Polkit::reset(const QUuid& dbUuid)
{
- m_encryptedMasterKeys.remove(dbUuid);
+ m_sessionKeys.remove(dbUuid);
+ nixUtils()->removeSecret(dbUuid.toString());
}
bool Polkit::isAvailable() const
@@ -89,67 +98,100 @@ bool Polkit::isAvailable() const
return m_available;
}
-QString Polkit::errorString() const
-{
- return m_error;
-}
-
void Polkit::reset()
{
- m_encryptedMasterKeys.clear();
+ m_sessionKeys.clear();
+ nixUtils()->removeAllSecrets();
}
-bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key)
+bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& data)
{
reset(dbUuid);
- // Generate a random iv/key pair to encrypt the master password with
- QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
- QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
- QByteArray keychainKeyValue = randomKey + randomIV;
+ // Prompt for a pin to use as session key
+ QByteArray key;
+ if (!promptPin(0, key)) {
+ return false;
+ }
+
+ auto iv = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Encrypt;
- if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
- m_error = QObject::tr("AES initialization failed");
+ if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
+ m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
- // Encrypt the master password
- QByteArray encryptedMasterKey = key;
- if (!aes256Encrypt.finish(encryptedMasterKey)) {
- m_error = QObject::tr("AES encrypt failed");
- qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
+ // Encrypt the database key
+ QByteArray encrypted = data;
+ if (!aes256Encrypt.finish(encrypted)) {
+ m_error = QObject::tr("Failed to encrypt key data.");
return false;
}
- // Add the iv/key pair into the linux keyring
- key_serial_t key_serial = add_key("user",
- getKeyName(dbUuid).toStdString().c_str(),
- keychainKeyValue.constData(),
- keychainKeyValue.size(),
- KEY_SPEC_PROCESS_KEYRING);
- if (key_serial < 0) {
- m_error = QObject::tr("Failed to store in Linux Keyring");
- qDebug() << "polkit keyring failed to store: " << errno;
- return false;
- }
+ // Store the session key and save the encrypted master key to the keyring
+ m_sessionKeys.insert(dbUuid, key);
+ nixUtils()->saveSecret(dbUuid.toString(), encrypted.prepend(iv));
- // Scrub the keys from ram
- Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
- Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
- Botan::secure_scrub_memory(keychainKeyValue.data(), keychainKeyValue.size());
-
- // Store encrypted master password and return
- m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
return true;
}
-bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
+bool Polkit::getKey(const QUuid& dbUuid, QByteArray& data)
{
- if (!m_polkit || !hasKey(dbUuid)) {
+ if (!m_available || !hasKey(dbUuid)) {
+ m_error = QObject::tr("No key is stored for this database.");
return false;
}
+ QByteArray key;
+ for (int pinAttempts = 1; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
+ if (!m_sessionKeys.contains(dbUuid)) {
+ // Request pin to obtain a session key
+ if (!promptPin(pinAttempts, key)) {
+ m_error = QObject::tr("Failed to obtain session key.");
+ return false;
+ }
+ } else {
+ // We already have the session key, prompt using polkit to authorize use
+ if (!promptPolkit()) {
+ // Error set in promptPolkit call
+ return false;
+ }
+ key = m_sessionKeys.value(dbUuid);
+ }
+
+ // Retrieve the encrypted master key from the OS secret store
+ QByteArray encData;
+ if (!nixUtils()->getSecret(dbUuid.toString(), encData)) {
+ m_error = QObject::tr("Failed to get credentials for quick unlock.");
+ return false;
+ }
+
+ const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
+ const auto& iv = encData.left(ivSize);
+
+ // Decrypt the data using the generated key and IV from above
+ SymmetricCipher cipher;
+ if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
+ m_error = QObject::tr("Failed to init KeePassXC crypto.");
+ return false;
+ }
+
+ // Attempt to decrypt the key data
+ data = encData.mid(ivSize);
+ if (cipher.finish(data)) {
+ // Decryption succeeded, store the session key used
+ m_sessionKeys.insert(dbUuid, key);
+ return true;
+ }
+ }
+
+ m_error = QObject::tr("Too many pin attempts.");
+ return false;
+}
+
+bool Polkit::promptPolkit()
+{
PolkitSubject subject;
subject.kind = "unix-process";
subject.details.insert("pid", static_cast(QCoreApplication::applicationPid()));
@@ -170,78 +212,26 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
if (result.isError()) {
auto msg = result.error().message();
m_error = QObject::tr("Polkit returned an error: %1").arg(msg);
- qDebug() << "polkit returned an error: " << msg;
return false;
}
PolkitAuthorizationResults authResult = result.value();
if (authResult.is_authorized) {
- QByteArray encryptedMasterKey = m_encryptedMasterKeys.value(dbUuid);
- key_serial_t keySerial =
- find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING);
-
- if (keySerial == -1) {
- m_error = QObject::tr("Could not locate key in keyring");
- qDebug() << "polkit keyring failed to find: " << errno;
- return false;
- }
-
- void* keychainBuffer;
- long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer);
-
- if (keychainDataSize == -1) {
- m_error = QObject::tr("Could not read key in keyring");
- qDebug() << "polkit keyring failed to read: " << errno;
- return false;
- }
-
- QByteArray keychainBytes(static_cast(keychainBuffer), keychainDataSize);
-
- Botan::secure_scrub_memory(keychainBuffer, keychainDataSize);
- free(keychainBuffer);
-
- QByteArray keychainKey = keychainBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
- QByteArray keychainIv = keychainBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
-
- SymmetricCipher aes256Decrypt;
- if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) {
- m_error = QObject::tr("AES initialization failed");
- qDebug() << "polkit aes init failed";
- return false;
- }
-
- key = encryptedMasterKey;
- if (!aes256Decrypt.finish(key)) {
- key.clear();
- m_error = QObject::tr("AES decrypt failed");
- qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString();
- return false;
- }
-
- // Scrub the keys from ram
- Botan::secure_scrub_memory(keychainKey.data(), keychainKey.size());
- Botan::secure_scrub_memory(keychainIv.data(), keychainIv.size());
- Botan::secure_scrub_memory(keychainBytes.data(), keychainBytes.size());
- Botan::secure_scrub_memory(encryptedMasterKey.data(), encryptedMasterKey.size());
-
return true;
}
// Failed to authenticate
if (authResult.is_challenge) {
- m_error = QObject::tr("No Polkit authentication agent was available");
+ m_error = QObject::tr("No Polkit authentication agent was available.");
} else {
- m_error = QObject::tr("Polkit authorization failed");
+ m_error = QObject::tr("Polkit authorization failed.");
}
-
return false;
}
bool Polkit::hasKey(const QUuid& dbUuid) const
{
- if (!m_encryptedMasterKeys.contains(dbUuid)) {
- return false;
- }
-
- return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1;
+ // Check if the OS has a secret stored for this database UUID
+ QByteArray tmp;
+ return nixUtils()->getSecret(dbUuid.toString(), tmp);
}
diff --git a/src/quickunlock/Polkit.h b/src/quickunlock/Polkit.h
index 7dfc2db7b..f3033e9c4 100644
--- a/src/quickunlock/Polkit.h
+++ b/src/quickunlock/Polkit.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 KeePassXC Team
+ * Copyright (C) 2025 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
@@ -15,36 +15,34 @@
* along with this program. If not, see .
*/
-#ifndef KEEPASSX_POLKIT_H
-#define KEEPASSX_POLKIT_H
+#pragma once
-#include "QuickUnlockInterface.h"
+#include "PinUnlock.h"
#include "polkit_dbus.h"
+
#include
#include
-class Polkit : public QuickUnlockInterface
+class Polkit : public PinUnlock
{
public:
Polkit();
~Polkit() override;
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 setKey(const QUuid& dbUuid, const QByteArray& data) override;
+ bool getKey(const QUuid& dbUuid, QByteArray& data) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
+ bool promptPolkit();
+
bool m_available;
- QString m_error;
- QHash m_encryptedMasterKeys;
+ QHash m_sessionKeys;
QScopedPointer m_polkit;
};
-
-#endif // KEEPASSX_POLKIT_H
diff --git a/src/quickunlock/PolkitDbusTypes.cpp b/src/quickunlock/PolkitDbusTypes.cpp
index a4305dc44..618b3c011 100644
--- a/src/quickunlock/PolkitDbusTypes.cpp
+++ b/src/quickunlock/PolkitDbusTypes.cpp
@@ -1,3 +1,20 @@
+/*
+ * Copyright (C) 2025 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 "PolkitDbusTypes.h"
void PolkitSubject::registerMetaType()
@@ -43,3 +60,32 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizati
argument.endStructure();
return argument;
}
+
+void PolkitActionDescription::registerMetaType()
+{
+ qRegisterMetaType("PolkitActionDescription");
+ qDBusRegisterMetaType();
+
+ qRegisterMetaType("PolkitActionDescriptionList");
+ qDBusRegisterMetaType();
+}
+
+QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action)
+{
+ argument.beginStructure();
+ argument << action.actionId << action.description << action.message << action.vendorName << action.vendorUrl
+ << action.iconName << action.implicitAny << action.implicitInactive << action.implicitActive
+ << action.annotations;
+ argument.endStructure();
+ return argument;
+}
+
+const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action)
+{
+ argument.beginStructure();
+ argument >> action.actionId >> action.description >> action.message >> action.vendorName >> action.vendorUrl
+ >> action.iconName >> action.implicitAny >> action.implicitInactive >> action.implicitActive
+ >> action.annotations;
+ argument.endStructure();
+ return argument;
+}
diff --git a/src/quickunlock/PolkitDbusTypes.h b/src/quickunlock/PolkitDbusTypes.h
index 83eb23889..8fb6daa31 100644
--- a/src/quickunlock/PolkitDbusTypes.h
+++ b/src/quickunlock/PolkitDbusTypes.h
@@ -1,5 +1,21 @@
-#ifndef KEEPASSX_POLKITDBUSTYPES_H
-#define KEEPASSX_POLKITDBUSTYPES_H
+/*
+ * Copyright (C) 2025 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 .
+ */
+
+#pragma once
#include
@@ -30,7 +46,30 @@ public:
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject);
};
+class PolkitActionDescription
+{
+public:
+ QString actionId;
+ QString description;
+ QString message;
+ QString vendorName;
+ QString vendorUrl;
+ QString iconName;
+ uint implicitAny;
+ uint implicitInactive;
+ uint implicitActive;
+ QMap annotations;
+
+ static void registerMetaType();
+
+ friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action);
+
+ friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action);
+};
+
+typedef QList PolkitActionDescriptionList;
+
Q_DECLARE_METATYPE(PolkitSubject);
Q_DECLARE_METATYPE(PolkitAuthorizationResults);
-
-#endif // KEEPASSX_POLKITDBUSTYPES_H
+Q_DECLARE_METATYPE(PolkitActionDescription);
+Q_DECLARE_METATYPE(PolkitActionDescriptionList);
diff --git a/src/quickunlock/QuickUnlockInterface.cpp b/src/quickunlock/QuickUnlockInterface.cpp
index 0e24736e8..bd365ed28 100644
--- a/src/quickunlock/QuickUnlockInterface.cpp
+++ b/src/quickunlock/QuickUnlockInterface.cpp
@@ -16,66 +16,55 @@
*/
#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;
-}
-
-bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const
-{
- Q_UNUSED(dbUuid)
- return false;
-}
-
-void NoQuickUnlock::reset(const QUuid& dbUuid)
-{
- Q_UNUSED(dbUuid)
+ return m_nativeInterface && m_nativeInterface->isAvailable();
}
diff --git a/src/quickunlock/QuickUnlockInterface.h b/src/quickunlock/QuickUnlockInterface.h
index 54aeb8a62..8ca09d47e 100644
--- a/src/quickunlock/QuickUnlockInterface.h
+++ b/src/quickunlock/QuickUnlockInterface.h
@@ -18,6 +18,7 @@
#ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H
#define KEEPASSXC_QUICKUNLOCKINTERFACE_H
+#include
#include
class QuickUnlockInterface
@@ -29,7 +30,6 @@ public:
virtual ~QuickUnlockInterface() = default;
virtual bool isAvailable() const = 0;
- virtual QString errorString() const = 0;
virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0;
virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0;
@@ -37,22 +37,32 @@ public:
virtual void reset(const QUuid& dbUuid) = 0;
virtual void reset() = 0;
+
+ virtual QString errorString() const
+ {
+ return m_error;
+ }
+
+protected:
+ QString m_error;
};
-class NoQuickUnlock : public QuickUnlockInterface
+class QuickUnlockManager final
{
+ Q_DISABLE_COPY(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;
- 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/TouchID.cpp b/src/quickunlock/TouchID.cpp
new file mode 100644
index 000000000..a36362b63
--- /dev/null
+++ b/src/quickunlock/TouchID.cpp
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2025 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 "quickunlock/TouchID.h"
+#include "gui/osutils/OSUtils.h"
+
+/**
+ * Store the serialized database key into the macOS key store. The OS handles encrypt/decrypt operations.
+ * https://developer.apple.com/documentation/security/keychain_services/keychain_items
+ */
+bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key)
+{
+ if (key.isEmpty()) {
+ qWarning("TouchID::setKey - provided key is empty");
+ return false;
+ }
+
+ return osUtils->saveSecret(dbUuid.toString(), key);
+}
+
+/**
+ * Retrieve serialized key data from the macOS Keychain after successful authentication
+ * with TouchID or Watch interface.
+ */
+bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key)
+{
+ key.clear();
+
+ if (!hasKey(dbUuid)) {
+ qWarning("TouchID::getKey - No stored key found");
+ return false;
+ }
+
+ return osUtils->getSecret(dbUuid.toString(), key);
+}
+
+bool TouchID::hasKey(const QUuid& dbUuid) const
+{
+ QByteArray tmp;
+ return osUtils->getSecret(dbUuid.toString(), tmp);
+}
+
+bool TouchID::isAvailable() const
+{
+ return macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::TouchId)
+ || macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::Watch)
+ || macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback);
+}
+
+void TouchID::reset(const QUuid& dbUuid)
+{
+ osUtils->removeSecret(dbUuid.toString());
+}
+
+void TouchID::reset()
+{
+ osUtils->removeAllSecrets();
+}
diff --git a/src/quickunlock/TouchID.h b/src/quickunlock/TouchID.h
index 74e5d9474..886710cbf 100644
--- a/src/quickunlock/TouchID.h
+++ b/src/quickunlock/TouchID.h
@@ -15,17 +15,14 @@
* along with this program. If not, see .
*/
-#ifndef KEEPASSX_TOUCHID_H
-#define KEEPASSX_TOUCHID_H
+#pragma once
#include "QuickUnlockInterface.h"
-#include
class TouchID : public QuickUnlockInterface
{
public:
bool isAvailable() const override;
- QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override;
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
@@ -33,17 +30,4 @@ public:
void reset(const QUuid& dbUuid = "") override;
void reset() override;
-
-private:
- static bool isWatchAvailable();
- static bool isTouchIdAvailable();
- static bool isPasswordFallbackPossible();
- bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID);
-
- static void deleteKeyEntry(const QString& accountName);
- static QString databaseKeyName(const QUuid& dbUuid);
-
- QHash m_encryptedMasterKeys;
};
-
-#endif // KEEPASSX_TOUCHID_H
diff --git a/src/quickunlock/TouchID.mm b/src/quickunlock/TouchID.mm
deleted file mode 100644
index f7ea18014..000000000
--- a/src/quickunlock/TouchID.mm
+++ /dev/null
@@ -1,408 +0,0 @@
-#include "quickunlock/TouchID.h"
-
-#include "crypto/Random.h"
-#include "crypto/SymmetricCipher.h"
-#include "crypto/CryptoHash.h"
-#include "config-keepassx.h"
-
-#include
-
-#include
-#include
-#include
-#include
-
-#include
-#include
-
-#define TOUCH_ID_ENABLE_DEBUG_LOGS() 0
-#if TOUCH_ID_ENABLE_DEBUG_LOGS()
-#define debug(...) qWarning(__VA_ARGS__)
-#else
-inline void debug(const char *message, ...)
-{
- Q_UNUSED(message);
-}
-#endif
-
-inline std::string StatusToErrorMessage(OSStatus status)
-{
- CFStringRef text = SecCopyErrorMessageString(status, NULL);
- if (!text) {
- return std::to_string(status);
- }
-
- auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
- std::string result;
- if (msg) {
- result = msg;
- }
- CFRelease(text);
- return result;
-}
-
-inline void LogStatusError(const char *message, OSStatus status)
-{
- if (!status) {
- return;
- }
-
- std::string msg = StatusToErrorMessage(status);
- debug("%s: %s", message, msg.c_str());
-}
-
-inline CFMutableDictionaryRef makeDictionary() {
- return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
-}
-
-//! Try to delete an existing keychain entry
-void TouchID::deleteKeyEntry(const QString& accountName)
-{
- NSString* nsAccountName = accountName.toNSString(); // The NSString is released by Qt
-
- // try to delete an existing entry
- CFMutableDictionaryRef query = makeDictionary();
- CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
- CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) nsAccountName);
- CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
-
- // get data from the KeyChain
- OSStatus status = SecItemDelete(query);
- LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status);
-}
-
-QString TouchID::databaseKeyName(const QUuid& dbUuid)
-{
- static const QString keyPrefix = "KeepassXC_TouchID_Keys_";
- return keyPrefix + dbUuid.toString();
-}
-
-QString TouchID::errorString() const
-{
- // TODO
- return "";
-}
-
-void TouchID::reset()
-{
- m_encryptedMasterKeys.clear();
-}
-
-
-
-
-bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID)
-{
- if (passwordKey.isEmpty()) {
- debug("TouchID::setKey - illegal arguments");
- return false;
- }
-
- if (m_encryptedMasterKeys.contains(dbUuid)) {
- debug("TouchID::setKey - Already stored key for this database");
- return true;
- }
-
- // generate random AES 256bit key and IV
- QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
- QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
-
- SymmetricCipher aes256Encrypt;
- if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
- debug("TouchID::setKey - AES initialisation failed");
- return false;
- }
-
- // encrypt and keep result in memory
- QByteArray encryptedMasterKey = passwordKey;
- if (!aes256Encrypt.finish(encryptedMasterKey)) {
- debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData());
- return false;
- }
-
- const QString keyName = databaseKeyName(dbUuid);
-
- deleteKeyEntry(keyName); // Try to delete the existing key entry
-
- // prepare adding secure entry to the macOS KeyChain
- CFErrorRef error = NULL;
-
- // We need both runtime and compile time checks here to solve the following problems:
- // - Not all flags are available in all OS versions, so we have to check it at compile time
- // - Requesting Biometry/TouchID/DevicePassword when to fingerprint sensor is available will result in runtime error
- SecAccessControlCreateFlags accessControlFlags = 0;
-#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
- // Needs a special check to work with SecItemAdd, when TouchID is not enrolled and the flag
- // is set, the method call fails with an error. But we want to still set this flag if TouchID is
- // enrolled but temporarily unavailable due to closed lid
- //
- // At least on a Hackintosh the enrolled-check does not work, there LAErrorBiometryNotAvailable gets returned instead of
- // LAErrorBiometryNotEnrolled.
- //
- // That's kinda unfortunate, because now you cannot know for sure if TouchID hardware is either temporarily unavailable or not present
- // at all, because LAErrorBiometryNotAvailable is used for both cases.
- //
- // So to make quick unlock fallbacks possible on these machines you have to try to save the key a second time without this flag, if the
- // first try fails with an error.
- if (!ignoreTouchID) {
- // Prefer the non-deprecated flag when available
- accessControlFlags = kSecAccessControlBiometryCurrentSet;
- }
-#elif XC_COMPILER_SUPPORT(TOUCH_ID)
- if (!ignoreTouchID) {
- accessControlFlags = kSecAccessControlTouchIDCurrentSet;
- }
-#endif
-
-#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
- accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
-#endif
-
-#if XC_COMPILER_SUPPORT(TOUCH_ID)
- if (isPasswordFallbackPossible()) {
- accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode;
- }
-#endif
-
- SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(
- kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
-
- if (sacObject == NULL || error != NULL) {
- NSError* e = (__bridge NSError*) error;
- debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
- return false;
- }
-
- NSString *accountName = keyName.toNSString(); // The NSString is released by Qt
-
- // prepare data (key) to be stored
- QByteArray keychainKeyValue = (randomKey + randomIV).toHex();
- CFDataRef keychainValueData =
- CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast(keychainKeyValue.data()),
- keychainKeyValue.length(), kCFAllocatorDefault);
-
- CFMutableDictionaryRef attributes = makeDictionary();
- CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
- CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
- CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData);
- CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
- CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
- CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
-
- // add to KeyChain
- OSStatus status = SecItemAdd(attributes, NULL);
- LogStatusError("TouchID::setKey - Status adding new entry", status);
-
- CFRelease(sacObject);
- CFRelease(attributes);
-
- // Cleanse the key information from the memory
- Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
- Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
-
- if (status != errSecSuccess) {
- return false;
- }
-
- // memorize which database the stored key is for
- m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
- debug("TouchID::setKey - Success!");
- return true;
-}
-
-/**
- * Generates a random AES 256bit key and uses it to encrypt the PasswordKey that
- * protects the database. The encrypted PasswordKey is kept in memory while the
- * AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch.
- */
-bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
-{
- if (!setKey(dbUuid,passwordKey, false)) {
- debug("TouchID::setKey failed with error trying fallback method without TouchID flag");
- return setKey(dbUuid, passwordKey, true);
- } else {
- return true;
- }
-}
-
-/**
- * Checks if an encrypted PasswordKey is available for the given database, tries to
- * decrypt it using the KeyChain and if successful, returns it.
- */
-bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
-{
- passwordKey.clear();
-
- if (!hasKey(dbUuid)) {
- debug("TouchID::getKey - No stored key found");
- return false;
- }
-
- // query the KeyChain for the AES key
- CFMutableDictionaryRef query = makeDictionary();
-
- const QString keyName = databaseKeyName(dbUuid);
- NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
- NSString* touchPromptMessage =
- QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
- .toNSString(); // The NSString is released by Qt
-
- CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
- CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
- CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
- CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef) touchPromptMessage);
-
- // get data from the KeyChain
- CFTypeRef dataTypeRef = NULL;
- OSStatus status = SecItemCopyMatching(query, &dataTypeRef);
- CFRelease(query);
-
- if (status == errSecUserCanceled) {
- // user canceled the authentication, return true with empty key
- debug("TouchID::getKey - User canceled authentication");
- return true;
- } else if (status != errSecSuccess || dataTypeRef == NULL) {
- LogStatusError("TouchID::getKey - key query error", status);
- return false;
- }
-
- CFDataRef valueData = static_cast(dataTypeRef);
- QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast(CFDataGetBytePtr(valueData)),
- CFDataGetLength(valueData)));
- CFRelease(dataTypeRef);
-
- // extract AES key and IV from data bytes
- QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
- QByteArray iv = dataBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
-
- SymmetricCipher aes256Decrypt;
- if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
- debug("TouchID::getKey - AES initialization failed");
- return false;
- }
-
- // decrypt PasswordKey from memory using AES
- passwordKey = m_encryptedMasterKeys[dbUuid];
- if (!aes256Decrypt.finish(passwordKey)) {
- passwordKey.clear();
- debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
- return false;
- }
-
- // Cleanse the key information from the memory
- Botan::secure_scrub_memory(key.data(), key.size());
- Botan::secure_scrub_memory(iv.data(), iv.size());
-
- return true;
-}
-
-bool TouchID::hasKey(const QUuid& dbUuid) const
-{
- return m_encryptedMasterKeys.contains(dbUuid);
-}
-
-// TODO: Both functions below should probably handle the returned errors to
-// provide more information on availability. E.g.: the closed laptop lid results
-// in an error (because touch id is not unavailable). That error could be
-// displayed to the user when we first check for availability instead of just
-// hiding the checkbox.
-
-//! @return true if Apple Watch is available for authentication.
-bool TouchID::isWatchAvailable()
-{
-#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
- @try {
- LAContext *context = [[LAContext alloc] init];
-
- LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
- NSError *error;
-
- bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
- [context release];
- if (error) {
- debug("Apple Wach available: %d (%ld / %s / %s)", canAuthenticate,
- (long)error.code, error.description.UTF8String,
- error.localizedDescription.UTF8String);
- } else {
- debug("Apple Wach available: %d", canAuthenticate);
- }
- return canAuthenticate;
- } @catch (NSException *) {
- return false;
- }
-#else
- return false;
-#endif
-}
-
-//! @return true if Touch ID is available for authentication.
-bool TouchID::isTouchIdAvailable()
-{
-#if XC_COMPILER_SUPPORT(TOUCH_ID)
- @try {
- LAContext *context = [[LAContext alloc] init];
-
- LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
- NSError *error;
-
- bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
- [context release];
- if (error) {
- debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate,
- (long)error.code, error.description.UTF8String,
- error.localizedDescription.UTF8String);
- } else {
- debug("Touch ID available: %d", canAuthenticate);
- }
- return canAuthenticate;
- } @catch (NSException *) {
- return false;
- }
-#else
- return false;
-#endif
-}
-
-bool TouchID::isPasswordFallbackPossible()
-{
-#if XC_COMPILER_SUPPORT(TOUCH_ID)
- @try {
- LAContext *context = [[LAContext alloc] init];
-
- LAPolicy policyCode = LAPolicyDeviceOwnerAuthentication;
- NSError *error;
-
- bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
- [context release];
- if (error) {
- debug("Password fallback available: %d (%ld / %s / %s)", canAuthenticate,
- (long)error.code, error.description.UTF8String,
- error.localizedDescription.UTF8String);
- } else {
- debug("Password fallback available: %d", canAuthenticate);
- }
- return canAuthenticate;
- } @catch (NSException *) {
- return false;
- }
-#else
- return false;
-#endif
-}
-
-//! @return true if either TouchID or Apple Watch is available at the moment.
-bool TouchID::isAvailable() const
-{
- // note: we cannot cache the check results because the configuration
- // is dynamic in its nature. User can close the laptop lid or take off
- // the watch, thus making one (or both) of the authentication types unavailable.
- return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible();
-}
-
-/**
- * Resets the inner state either for all or for the given database
- */
-void TouchID::reset(const QUuid& dbUuid)
-{
- m_encryptedMasterKeys.remove(dbUuid);
-}
diff --git a/src/quickunlock/WindowsHello.cpp b/src/quickunlock/WindowsHello.cpp
index 890e3499a..539f00c64 100644
--- a/src/quickunlock/WindowsHello.cpp
+++ b/src/quickunlock/WindowsHello.cpp
@@ -17,8 +17,9 @@
#include "WindowsHello.h"
-#include
+#include
#include
+#include
#include
#include
#include
@@ -28,12 +29,14 @@
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
+#include "gui/osutils/OSUtils.h"
#include
#include
using namespace winrt;
using namespace Windows::Foundation;
+using namespace Windows::Foundation::Collections;
using namespace Windows::Security::Credentials;
using namespace Windows::Security::Cryptography;
using namespace Windows::Storage::Streams;
@@ -43,17 +46,20 @@ namespace
const std::wstring s_winHelloKeyName{L"keepassxc_winhello"};
int g_promptFocusCount = 0;
- void queueSecurityPromptFocus(int delay = 500)
+ void queueSecurityPromptFocus(bool initial, int delay = 500)
{
+ if (initial) {
+ g_promptFocusCount = 0;
+ }
+
QTimer::singleShot(delay, [] {
auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr);
if (hWnd) {
::SetForegroundWindow(hWnd);
} else if (++g_promptFocusCount <= 3) {
- queueSecurityPromptFocus();
- return;
+ qDebug("WindowsHello - Could not find security prompt window");
+ queueSecurityPromptFocus(false);
}
- g_promptFocusCount = 0;
});
}
@@ -105,14 +111,9 @@ bool WindowsHello::isAvailable() const
return task.get();
}
-QString WindowsHello::errorString() const
-{
- return m_error;
-}
-
bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
{
- queueSecurityPromptFocus();
+ queueSecurityPromptFocus(true);
// Generate a random challenge that will be signed by Windows Hello
// to create the key. The challenge is also used as the IV.
@@ -120,6 +121,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
auto challenge = Random::instance()->randomArray(ivSize);
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
+ m_error = QObject::tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled.");
return false;
}
@@ -137,28 +139,28 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
// Prepend the challenge/IV to the encrypted data
encrypted.prepend(challenge);
- m_encryptedKeys.insert(dbUuid, encrypted);
- return true;
+ return osUtils->saveSecret(dbUuid.toString(), encrypted);
}
bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
{
data.clear();
- if (!hasKey(dbUuid)) {
- m_error = QObject::tr("Failed to get Windows Hello credential.");
+ QByteArray keydata;
+ if (!osUtils->getSecret(dbUuid.toString(), keydata)) {
+ m_error = QObject::tr("Failed to retrieve Windows Hello credential.");
return false;
}
- queueSecurityPromptFocus();
+ queueSecurityPromptFocus(true);
// Read the previously used challenge and encrypted data
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
- const auto& keydata = m_encryptedKeys.value(dbUuid);
auto challenge = keydata.left(ivSize);
auto encrypted = keydata.mid(ivSize);
- QByteArray key;
+ QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
+ // Error is set in deriveEncryptionKey
return false;
}
@@ -182,15 +184,16 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
void WindowsHello::reset(const QUuid& dbUuid)
{
- m_encryptedKeys.remove(dbUuid);
+ osUtils->removeSecret(dbUuid.toString());
}
bool WindowsHello::hasKey(const QUuid& dbUuid) const
{
- return m_encryptedKeys.contains(dbUuid);
+ QByteArray tmp;
+ return osUtils->getSecret(dbUuid.toString(), tmp);
}
void WindowsHello::reset()
{
- m_encryptedKeys.clear();
+ osUtils->removeAllSecrets();
}
diff --git a/src/quickunlock/WindowsHello.h b/src/quickunlock/WindowsHello.h
index 9da6e4160..cff2fe13b 100644
--- a/src/quickunlock/WindowsHello.h
+++ b/src/quickunlock/WindowsHello.h
@@ -20,26 +20,22 @@
#include "QuickUnlockInterface.h"
-#include
-#include
-
class WindowsHello : public QuickUnlockInterface
{
public:
WindowsHello() = default;
+
bool isAvailable() const override;
- QString errorString() const override;
- void reset() 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;
+
void reset(const QUuid& dbUuid) override;
+ void reset() override;
private:
- QString m_error;
- QHash m_encryptedKeys;
- Q_DISABLE_COPY(WindowsHello);
+ Q_DISABLE_COPY(WindowsHello)
};
#endif // KEEPASSXC_WINDOWSHELLO_H
diff --git a/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml b/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
index d46d71d2a..b8f5328f8 100644
--- a/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
+++ b/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
@@ -12,5 +12,10 @@
+
+
+
+
+