This commit is contained in:
Jonathan White 2026-03-08 12:34:32 +08:00 committed by GitHub
commit 4ed0cd0fba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1344 additions and 792 deletions

View file

@ -657,10 +657,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>
@ -705,6 +701,14 @@
<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>Enable database quick unlock by default</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remember quick unlock after database is closed</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>AttachmentWidget</name> <name>AttachmentWidget</name>
@ -1654,10 +1658,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>
@ -1735,10 +1735,6 @@ To prevent this error from appearing, you must go to &quot;Database Settings / S
<source>Cannot use database file as key file</source> <source>Cannot use database file as key file</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>authenticate to access the database</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Failed to authenticate with Quick Unlock: %1</source> <source>Failed to authenticate with Quick Unlock: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1791,6 +1787,14 @@ 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>Reset</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Database</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Hardware keys found, but no slots are configured.</source> <source>Hardware keys found, but no slots are configured.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1799,6 +1803,10 @@ Are you sure you want to continue with this file?.</source>
<source>Press ESC again to close this database</source> <source>Press ESC again to close this database</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Quick Unlock</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>DatabaseSettingWidgetMetaData</name> <name>DatabaseSettingWidgetMetaData</name>
@ -9134,46 +9142,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>
@ -9182,10 +9154,6 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Failed to encrypt key data.</source> <source>Failed to encrypt key data.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Failed to get Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Failed to decrypt key data.</source> <source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -9349,7 +9317,35 @@ This option is deprecated, use --set-key-file instead.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Format to use when exporting. Available choices are &apos;xml&apos;, &apos;csv&apos; or &apos;html&apos;. Defaults to &apos;xml&apos;.</source> <source>Quick Unlock Pin Entry</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>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> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
@ -9433,6 +9429,34 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Confirm Replace Entry References</source> <source>Confirm Replace Entry References</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Format to use when exporting. Available choices are &apos;xml&apos;, &apos;csv&apos; or &apos;html&apos;. Defaults to &apos;xml&apos;.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enter a %1%2 digit pin to use for quick unlock:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to derive key using Argon2</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Too many pin attempts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No key is stored for this database.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to obtain session key.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to retrieve Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>QtIOCompressor</name> <name>QtIOCompressor</name>

View file

@ -217,6 +217,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)
@ -227,40 +228,41 @@ if(APPLE)
gui/osutils/macutils/ScreenLockListenerMac.cpp gui/osutils/macutils/ScreenLockListenerMac.cpp
gui/osutils/macutils/AppKitImpl.mm gui/osutils/macutils/AppKitImpl.mm
gui/osutils/macutils/AppKit.h gui/osutils/macutils/AppKit.h
quickunlock/TouchID.mm) quickunlock/TouchID.cpp)
# TODO: Remove -Wno-error once deprecation warnings have been resolved.
set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast")
endif() endif()
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
list(APPEND gui_SOURCES list(APPEND gui_SOURCES
gui/osutils/nixutils/ScreenLockListenerDBus.cpp gui/osutils/nixutils/ScreenLockListenerDBus.cpp
gui/osutils/nixutils/NixUtils.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) if(WITH_XC_X11)
list(APPEND gui_SOURCES list(APPEND gui_SOURCES
gui/osutils/nixutils/X11Funcs.cpp) gui/osutils/nixutils/X11Funcs.cpp)
endif() 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 qt5_add_dbus_adaptor(gui_SOURCES
gui/org.keepassxc.KeePassXC.MainWindow.xml gui/org.keepassxc.KeePassXC.MainWindow.xml
gui/MainWindow.h gui/MainWindow.h
MainWindow) 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) find_library(KEYUTILS_LIBRARIES NAMES keyutils)
if(NOT KEYUTILS_LIBRARIES) if(NOT KEYUTILS_LIBRARIES)
message(FATAL_ERROR "Could not find libkeyutils") message(FATAL_ERROR "Could not find libkeyutils")

View file

@ -155,7 +155,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}}, {Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}}, {Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}}, {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 // Browser
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}}, {Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},

View file

@ -136,6 +136,7 @@ public:
Security_NoConfirmMoveEntryToRecycleBin, Security_NoConfirmMoveEntryToRecycleBin,
Security_EnableCopyOnDoubleClick, Security_EnableCopyOnDoubleClick,
Security_QuickUnlock, Security_QuickUnlock,
Security_QuickUnlockRemember,
Security_DatabasePasswordMinimumQuality, Security_DatabasePasswordMinimumQuality,
Browser_Enabled, Browser_Enabled,

View file

@ -163,6 +163,13 @@ QVariantMap Argon2Kdf::writeParameters()
bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const 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.clear();
result.resize(32); result.resize(32);
// Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length // Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length

View file

@ -348,8 +348,15 @@ 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->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)) { for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings(); page.loadSettings();
@ -469,9 +476,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());
}
// 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

@ -148,7 +148,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>40</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -174,7 +174,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -210,7 +210,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -250,7 +250,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -315,7 +315,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>40</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -483,7 +483,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -516,7 +516,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>40</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -648,7 +648,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -681,7 +681,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -743,7 +743,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>40</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -971,7 +971,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -1019,7 +1019,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>40</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -1076,7 +1076,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>30</width> <width>30</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>

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">
@ -138,7 +138,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>40</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -168,7 +168,14 @@
<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)</string> <string>Enable database quick unlock by default</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="quickUnlockRememberCheckBox">
<property name="text">
<string>Remember quick unlock after database is closed</string>
</property> </property>
</widget> </widget>
</item> </item>

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)
{ {
@ -189,7 +188,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();
@ -294,6 +293,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
@ -335,16 +335,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();
@ -386,10 +382,13 @@ 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();
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(); m_ui->messageWidget->hideMessage();
} }
@ -434,13 +433,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);
@ -627,9 +629,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
@ -656,7 +664,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen()
void DatabaseOpenWidget::triggerQuickUnlock() void DatabaseOpenWidget::triggerQuickUnlock()
{ {
if (isOnQuickUnlockScreen()) { if (isOnQuickUnlockScreen() && !unlockingDatabase()) {
m_ui->quickUnlockButton->click(); m_ui->quickUnlockButton->click();
} }
} }
@ -668,11 +676,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>
@ -46,21 +45,17 @@ 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(); void triggerQuickUnlock();
bool unlockingDatabase(); bool unlockingDatabase();
void showMessage(const QString& text, MessageWidget::MessageType type, int autoHideTimeout); 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: signals:
void dialogFinished(bool accepted); void dialogFinished(bool accepted);
@ -85,14 +80,20 @@ private slots:
void closeDatabase(); void closeDatabase();
void pollHardwareKey(bool manualTrigger = false, int delay = 0); void pollHardwareKey(bool manualTrigger = false, int delay = 0);
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_triedToQuit = false; bool m_triedToQuit = false;
QTimer m_hideTimer; QTimer m_hideTimer;

View file

@ -180,7 +180,7 @@
<number>0</number> <number>0</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>10</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="passwordLabel"> <widget class="QLabel" name="passwordLabel">
@ -192,7 +192,7 @@
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing"> <property name="spacing">
<number>2</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="PasswordWidget" name="editPassword" native="true"> <widget class="PasswordWidget" name="editPassword" native="true">
@ -250,13 +250,13 @@
<number>0</number> <number>0</number>
</property> </property>
<property name="topMargin"> <property name="topMargin">
<number>0</number> <number>10</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>0</number> <number>0</number>
</property> </property>
<property name="bottomMargin"> <property name="bottomMargin">
<number>10</number> <number>15</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="selectKeyFileLabel"> <widget class="QLabel" name="selectKeyFileLabel">
@ -399,7 +399,7 @@
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>40</width> <width>40</width>
<height>20</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -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>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,17 +587,81 @@
<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> <item>
<widget class="QPushButton" name="resetQuickUnlockButton"> <spacer name="verticalSpacer_4">
<property name="text"> <property name="orientation">
<string>Cancel</string> <enum>Qt::Vertical</enum>
</property> </property>
</widget> <property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>4</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="resetQuickUnlockButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="text">
<string>Reset</string>
</property>
</widget>
</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>4</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="closeQuickUnlockButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
<property name="text">
<string>Close Database</string>
</property>
</widget>
</item>
</layout>
</item> </item>
<item> <item>
<spacer name="verticalSpacer_8"> <spacer name="verticalSpacer_8">
@ -645,8 +754,6 @@
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops> <tabstops>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>editPassword</tabstop> <tabstop>editPassword</tabstop>
<tabstop>keyFileLineEdit</tabstop> <tabstop>keyFileLineEdit</tabstop>
<tabstop>buttonBrowseFile</tabstop> <tabstop>buttonBrowseFile</tabstop>
@ -654,7 +761,11 @@
<tabstop>hardwareKeyCombo</tabstop> <tabstop>hardwareKeyCombo</tabstop>
<tabstop>refreshHardwareKeys</tabstop> <tabstop>refreshHardwareKeys</tabstop>
<tabstop>addKeyFileLinkLabel</tabstop> <tabstop>addKeyFileLinkLabel</tabstop>
<tabstop>enableQuickUnlockCheckBox</tabstop>
<tabstop>buttonBox</tabstop> <tabstop>buttonBox</tabstop>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>closeQuickUnlockButton</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <connections/>

View file

@ -1978,8 +1978,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event)
event->ignore(); event->ignore();
return; return;
} }
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

@ -72,6 +72,14 @@ public:
virtual bool canPreventScreenCapture() const = 0; virtual bool canPreventScreenCapture() const = 0;
virtual bool setPreventScreenCapture(QWindow* window, bool allow) const; 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: signals:
void globalShortcutTriggered(const QString& name, const QString& search = {}); void globalShortcutTriggered(const QString& name, const QString& search = {});

View file

@ -17,14 +17,22 @@
*/ */
#import "AppKitImpl.h" #import "AppKitImpl.h"
#import "MacUtils.h"
#import <QWindow> #import <QWindow>
#import <QMenu> #import <QMenu>
#import <QMenuBar> #import <QMenuBar>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <Security/Security.h>
#if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3 #if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3
#import <ScreenCaptureKit/ScreenCaptureKit.h> #import <ScreenCaptureKit/ScreenCaptureKit.h>
#endif #endif
#include "config-keepassx.h"
@implementation AppKitImpl @implementation AppKitImpl
- (id) initWithObject:(AppKit*)appkit - (id) initWithObject:(AppKit*)appkit
@ -340,3 +348,222 @@ void AppKit::configureWindowAndHelpMenus(QMainWindow* window, QMenu* helpMenu)
{ {
[static_cast<id>(self) configureWindowAndHelpMenus:window helpMenu:helpMenu]; [static_cast<id>(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<NSError*>(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<const UInt8*>(keyBase64.data()),
keyBase64.length(), kCFAllocatorDefault);
auto attributes = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(attributes, kSecAttrAccount, static_cast<CFStringRef>(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<CFStringRef>(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<CFDataRef>(dataTypeRef);
secretData = QByteArray::fromBase64(QByteArray(reinterpret_cast<const char*>(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<CFStringRef>(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<NSArray*>(result)) {
NSString* account = item[static_cast<id>(kSecAttrAccount)];
if (account && [account hasPrefix:s_touchIdKeyPrefix.toNSString()]) {
auto delQuery = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(delQuery, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(delQuery, kSecAttrAccount, static_cast<CFStringRef>(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;
}
}

View file

@ -68,6 +68,21 @@ public:
bool canPreventScreenCapture() const override; bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) 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: signals:
void userSwitched(); void userSwitched();

View file

@ -30,6 +30,11 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <QStyle> #include <QStyle>
#include <QTextStream> #include <QTextStream>
extern "C" {
#include <keyutils.h>
}
#ifdef WITH_XC_X11 #ifdef WITH_XC_X11
#include <QX11Info> #include <QX11Info>
@ -411,3 +416,74 @@ quint64 NixUtils::getProcessStartTime() const
qDebug() << "nixutils: failed to find ')' in " << processStatPath; qDebug() << "nixutils: failed to find ')' in " << processStatPath;
return 0; 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;
}

View file

@ -52,6 +52,11 @@ public:
quint64 getProcessStartTime() const; 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: private slots:
void handleColorSchemeRead(QDBusVariant value); void handleColorSchemeRead(QDBusVariant value);
void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value); void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value);

View file

@ -20,11 +20,24 @@
#include <QApplication> #include <QApplication>
#include <QDir> #include <QDir>
#include <QSettings> #include <QSettings>
#include <QUuid>
#include <QWindow> #include <QWindow>
#include <Windows.h> #include <Windows.h>
#include <winrt/base.h>
#include <winrt/windows.foundation.collections.h>
#include <winrt/windows.security.credentials.h>
#undef MessageBox #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> WinUtils::m_instance = nullptr; QPointer<WinUtils> WinUtils::m_instance = nullptr;
WinUtils* WinUtils::instance() WinUtils* WinUtils::instance()
@ -361,3 +374,59 @@ DWORD WinUtils::qtToNativeModifiers(Qt::KeyboardModifiers modifiers)
return nativeModifiers; 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;
}

View file

@ -61,6 +61,11 @@ public:
bool canPreventScreenCapture() const override; bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) 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: protected:
explicit WinUtils(QObject* parent = nullptr); explicit WinUtils(QObject* parent = nullptr);
~WinUtils() override = default; ~WinUtils() override = default;

View file

@ -0,0 +1,207 @@
/*
* Copyright (C) 2025 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 "crypto/kdf/Argon2Kdf.h"
#include "gui/osutils/OSUtils.h"
#include <QInputDialog>
#include <QRegularExpression>
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();
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (C) 2025 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;
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<QUuid, QPair<int, QByteArray>> m_encryptedKeys;
Q_DISABLE_COPY(PinUnlock)
};
#endif // KEEPASSXC_PINUNLOCK_H

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -23,8 +23,8 @@
#include "gui/osutils/nixutils/NixUtils.h" #include "gui/osutils/nixutils/NixUtils.h"
#include <QDebug> #include <QDebug>
#include <QFile>
#include <QtDBus> #include <QtDBus>
#include <botan/mem_ops.h> #include <botan/mem_ops.h>
#include <cerrno> #include <cerrno>
@ -35,19 +35,11 @@ extern "C" {
const QString polkit_service = "org.freedesktop.PolicyKit1"; const QString polkit_service = "org.freedesktop.PolicyKit1";
const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority"; 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() Polkit::Polkit()
{ {
PolkitSubject::registerMetaType(); PolkitSubject::registerMetaType();
PolkitAuthorizationResults::registerMetaType(); PolkitAuthorizationResults::registerMetaType();
PolkitActionDescription::registerMetaType();
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden /* 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 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(); m_available = bus.isConnected();
if (!m_available) { 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; return;
} }
m_available = bus.interface()->isServiceRegistered(polkit_service); m_available = bus.interface()->isServiceRegistered(polkit_service);
if (!m_available) { if (!m_available) {
qDebug() << "polkit: Polkit is not registered on dbus"; qWarning() << "polkit: Polkit is not registered on dbus";
return; return;
} }
// Initiate the Polkit dbus interface
m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus)); 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() Polkit::~Polkit()
@ -81,7 +89,8 @@ Polkit::~Polkit()
void Polkit::reset(const QUuid& dbUuid) void Polkit::reset(const QUuid& dbUuid)
{ {
m_encryptedMasterKeys.remove(dbUuid); m_sessionKeys.remove(dbUuid);
nixUtils()->removeSecret(dbUuid.toString());
} }
bool Polkit::isAvailable() const bool Polkit::isAvailable() const
@ -89,67 +98,100 @@ bool Polkit::isAvailable() const
return m_available; return m_available;
} }
QString Polkit::errorString() const
{
return m_error;
}
void Polkit::reset() 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); reset(dbUuid);
// Generate a random iv/key pair to encrypt the master password with // Prompt for a pin to use as session key
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); QByteArray key;
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); if (!promptPin(0, key)) {
QByteArray keychainKeyValue = randomKey + randomIV; return false;
}
auto iv = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Encrypt; SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
m_error = QObject::tr("AES initialization failed"); m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false; return false;
} }
// Encrypt the master password // Encrypt the database key
QByteArray encryptedMasterKey = key; QByteArray encrypted = data;
if (!aes256Encrypt.finish(encryptedMasterKey)) { if (!aes256Encrypt.finish(encrypted)) {
m_error = QObject::tr("AES encrypt failed"); m_error = QObject::tr("Failed to encrypt key data.");
qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
return false; return false;
} }
// Add the iv/key pair into the linux keyring // Store the session key and save the encrypted master key to the keyring
key_serial_t key_serial = add_key("user", m_sessionKeys.insert(dbUuid, key);
getKeyName(dbUuid).toStdString().c_str(), nixUtils()->saveSecret(dbUuid.toString(), encrypted.prepend(iv));
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;
}
// 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; 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; 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; PolkitSubject subject;
subject.kind = "unix-process"; subject.kind = "unix-process";
subject.details.insert("pid", static_cast<uint>(QCoreApplication::applicationPid())); subject.details.insert("pid", static_cast<uint>(QCoreApplication::applicationPid()));
@ -170,78 +212,26 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
if (result.isError()) { if (result.isError()) {
auto msg = result.error().message(); auto msg = result.error().message();
m_error = QObject::tr("Polkit returned an error: %1").arg(msg); m_error = QObject::tr("Polkit returned an error: %1").arg(msg);
qDebug() << "polkit returned an error: " << msg;
return false; return false;
} }
PolkitAuthorizationResults authResult = result.value(); PolkitAuthorizationResults authResult = result.value();
if (authResult.is_authorized) { 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<const char*>(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; return true;
} }
// Failed to authenticate // Failed to authenticate
if (authResult.is_challenge) { 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 { } else {
m_error = QObject::tr("Polkit authorization failed"); m_error = QObject::tr("Polkit authorization failed.");
} }
return false; return false;
} }
bool Polkit::hasKey(const QUuid& dbUuid) const bool Polkit::hasKey(const QUuid& dbUuid) const
{ {
if (!m_encryptedMasterKeys.contains(dbUuid)) { // Check if the OS has a secret stored for this database UUID
return false; QByteArray tmp;
} return nixUtils()->getSecret(dbUuid.toString(), tmp);
return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1;
} }

View file

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org> * Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@ -15,36 +15,34 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef KEEPASSX_POLKIT_H #pragma once
#define KEEPASSX_POLKIT_H
#include "QuickUnlockInterface.h" #include "PinUnlock.h"
#include "polkit_dbus.h" #include "polkit_dbus.h"
#include <QHash> #include <QHash>
#include <QScopedPointer> #include <QScopedPointer>
class Polkit : public QuickUnlockInterface class Polkit : public PinUnlock
{ {
public: public:
Polkit(); Polkit();
~Polkit() override; ~Polkit() override;
bool isAvailable() const override; bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override; bool setKey(const QUuid& dbUuid, const QByteArray& data) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool getKey(const QUuid& dbUuid, QByteArray& data) override;
bool hasKey(const QUuid& dbUuid) const override; bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override; void reset(const QUuid& dbUuid) override;
void reset() override; void reset() override;
private: private:
bool promptPolkit();
bool m_available; bool m_available;
QString m_error; QHash<QUuid, QByteArray> m_sessionKeys;
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
QScopedPointer<org::freedesktop::PolicyKit1::Authority> m_polkit; QScopedPointer<org::freedesktop::PolicyKit1::Authority> m_polkit;
}; };
#endif // KEEPASSX_POLKIT_H

View file

@ -1,3 +1,20 @@
/*
* Copyright (C) 2025 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 "PolkitDbusTypes.h" #include "PolkitDbusTypes.h"
void PolkitSubject::registerMetaType() void PolkitSubject::registerMetaType()
@ -43,3 +60,32 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizati
argument.endStructure(); argument.endStructure();
return argument; return argument;
} }
void PolkitActionDescription::registerMetaType()
{
qRegisterMetaType<PolkitActionDescription>("PolkitActionDescription");
qDBusRegisterMetaType<PolkitActionDescription>();
qRegisterMetaType<PolkitActionDescriptionList>("PolkitActionDescriptionList");
qDBusRegisterMetaType<PolkitActionDescriptionList>();
}
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;
}

View file

@ -1,5 +1,21 @@
#ifndef KEEPASSX_POLKITDBUSTYPES_H /*
#define KEEPASSX_POLKITDBUSTYPES_H * Copyright (C) 2025 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/>.
*/
#pragma once
#include <QtDBus> #include <QtDBus>
@ -30,7 +46,30 @@ public:
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject); 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<QString, QString> annotations;
static void registerMetaType();
friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action);
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action);
};
typedef QList<PolkitActionDescription> PolkitActionDescriptionList;
Q_DECLARE_METATYPE(PolkitSubject); Q_DECLARE_METATYPE(PolkitSubject);
Q_DECLARE_METATYPE(PolkitAuthorizationResults); Q_DECLARE_METATYPE(PolkitAuthorizationResults);
Q_DECLARE_METATYPE(PolkitActionDescription);
#endif // KEEPASSX_POLKITDBUSTYPES_H Q_DECLARE_METATYPE(PolkitActionDescriptionList);

View file

@ -16,66 +16,55 @@
*/ */
#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
{
Q_UNUSED(dbUuid)
return false;
}
void NoQuickUnlock::reset(const QUuid& dbUuid)
{
Q_UNUSED(dbUuid)
} }

View file

@ -18,6 +18,7 @@
#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
@ -29,7 +30,6 @@ public:
virtual ~QuickUnlockInterface() = default; virtual ~QuickUnlockInterface() = default;
virtual bool isAvailable() const = 0; virtual bool isAvailable() const = 0;
virtual QString errorString() const = 0;
virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0; virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0;
virtual bool getKey(const QUuid& dbUuid, 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(const QUuid& dbUuid) = 0;
virtual void reset() = 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: 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;
void reset(const QUuid& dbUuid) override; private:
void reset() override; QSharedPointer<QuickUnlockInterface> m_nativeInterface;
QSharedPointer<QuickUnlockInterface> m_fallbackInterface;
}; };
QuickUnlockInterface* getQuickUnlock(); QuickUnlockManager* getQuickUnlock();
#endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H #endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H

View file

@ -0,0 +1,72 @@
/*
* Copyright (C) 2025 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 "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();
}

View file

@ -15,17 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef KEEPASSX_TOUCHID_H #pragma once
#define KEEPASSX_TOUCHID_H
#include "QuickUnlockInterface.h" #include "QuickUnlockInterface.h"
#include <QHash>
class TouchID : public QuickUnlockInterface class TouchID : public QuickUnlockInterface
{ {
public: public:
bool isAvailable() const override; bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override; bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override;
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override; bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
@ -33,17 +30,4 @@ public:
void reset(const QUuid& dbUuid = "") override; void reset(const QUuid& dbUuid = "") override;
void reset() 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<QUuid, QByteArray> m_encryptedMasterKeys;
}; };
#endif // KEEPASSX_TOUCHID_H

View file

@ -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 <botan/mem_ops.h>
#include <Foundation/Foundation.h>
#include <CoreFoundation/CoreFoundation.h>
#include <LocalAuthentication/LocalAuthentication.h>
#include <Security/Security.h>
#include <QCoreApplication>
#include <QString>
#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<UInt8 *>(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<CFDataRef>(dataTypeRef);
QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(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);
}

View file

@ -17,8 +17,9 @@
#include "WindowsHello.h" #include "WindowsHello.h"
#include <Userconsentverifierinterop.h> #include <Windows.h>
#include <winrt/base.h> #include <winrt/base.h>
#include <winrt/windows.foundation.collections.h>
#include <winrt/windows.foundation.h> #include <winrt/windows.foundation.h>
#include <winrt/windows.security.credentials.h> #include <winrt/windows.security.credentials.h>
#include <winrt/windows.security.cryptography.h> #include <winrt/windows.security.cryptography.h>
@ -28,12 +29,14 @@
#include "crypto/CryptoHash.h" #include "crypto/CryptoHash.h"
#include "crypto/Random.h" #include "crypto/Random.h"
#include "crypto/SymmetricCipher.h" #include "crypto/SymmetricCipher.h"
#include "gui/osutils/OSUtils.h"
#include <QTimer> #include <QTimer>
#include <QWindow> #include <QWindow>
using namespace winrt; using namespace winrt;
using namespace Windows::Foundation; using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Security::Credentials; using namespace Windows::Security::Credentials;
using namespace Windows::Security::Cryptography; using namespace Windows::Security::Cryptography;
using namespace Windows::Storage::Streams; using namespace Windows::Storage::Streams;
@ -43,17 +46,20 @@ namespace
const std::wstring s_winHelloKeyName{L"keepassxc_winhello"}; const std::wstring s_winHelloKeyName{L"keepassxc_winhello"};
int g_promptFocusCount = 0; int g_promptFocusCount = 0;
void queueSecurityPromptFocus(int delay = 500) void queueSecurityPromptFocus(bool initial, int delay = 500)
{ {
if (initial) {
g_promptFocusCount = 0;
}
QTimer::singleShot(delay, [] { QTimer::singleShot(delay, [] {
auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr); auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr);
if (hWnd) { if (hWnd) {
::SetForegroundWindow(hWnd); ::SetForegroundWindow(hWnd);
} else if (++g_promptFocusCount <= 3) { } else if (++g_promptFocusCount <= 3) {
queueSecurityPromptFocus(); qDebug("WindowsHello - Could not find security prompt window");
return; queueSecurityPromptFocus(false);
} }
g_promptFocusCount = 0;
}); });
} }
@ -105,14 +111,9 @@ bool WindowsHello::isAvailable() const
return task.get(); return task.get();
} }
QString WindowsHello::errorString() const
{
return m_error;
}
bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data) bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
{ {
queueSecurityPromptFocus(); queueSecurityPromptFocus(true);
// Generate a random challenge that will be signed by Windows Hello // Generate a random challenge that will be signed by Windows Hello
// to create the key. The challenge is also used as the IV. // 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); auto challenge = Random::instance()->randomArray(ivSize);
QByteArray key; QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) { 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; return false;
} }
@ -137,28 +139,28 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
// Prepend the challenge/IV to the encrypted data // Prepend the challenge/IV to the encrypted data
encrypted.prepend(challenge); encrypted.prepend(challenge);
m_encryptedKeys.insert(dbUuid, encrypted); return osUtils->saveSecret(dbUuid.toString(), encrypted);
return true;
} }
bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data) bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
{ {
data.clear(); data.clear();
if (!hasKey(dbUuid)) { QByteArray keydata;
m_error = QObject::tr("Failed to get Windows Hello credential."); if (!osUtils->getSecret(dbUuid.toString(), keydata)) {
m_error = QObject::tr("Failed to retrieve Windows Hello credential.");
return false; return false;
} }
queueSecurityPromptFocus(); queueSecurityPromptFocus(true);
// Read the previously used challenge and encrypted data // Read the previously used challenge and encrypted data
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& keydata = m_encryptedKeys.value(dbUuid);
auto challenge = keydata.left(ivSize); auto challenge = keydata.left(ivSize);
auto encrypted = keydata.mid(ivSize); auto encrypted = keydata.mid(ivSize);
QByteArray key;
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) { if (!deriveEncryptionKey(challenge, key, m_error)) {
// Error is set in deriveEncryptionKey
return false; return false;
} }
@ -182,15 +184,16 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
void WindowsHello::reset(const QUuid& dbUuid) void WindowsHello::reset(const QUuid& dbUuid)
{ {
m_encryptedKeys.remove(dbUuid); osUtils->removeSecret(dbUuid.toString());
} }
bool WindowsHello::hasKey(const QUuid& dbUuid) const bool WindowsHello::hasKey(const QUuid& dbUuid) const
{ {
return m_encryptedKeys.contains(dbUuid); QByteArray tmp;
return osUtils->getSecret(dbUuid.toString(), tmp);
} }
void WindowsHello::reset() void WindowsHello::reset()
{ {
m_encryptedKeys.clear(); osUtils->removeAllSecrets();
} }

View file

@ -20,26 +20,22 @@
#include "QuickUnlockInterface.h" #include "QuickUnlockInterface.h"
#include <QHash>
#include <QObject>
class WindowsHello : public QuickUnlockInterface class WindowsHello : public QuickUnlockInterface
{ {
public: public:
WindowsHello() = default; WindowsHello() = default;
bool isAvailable() const override; bool isAvailable() const override;
QString errorString() const override;
void reset() override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override; bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override; bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override; void reset(const QUuid& dbUuid) override;
void reset() override;
private: private:
QString m_error; Q_DISABLE_COPY(WindowsHello)
QHash<QUuid, QByteArray> m_encryptedKeys;
Q_DISABLE_COPY(WindowsHello);
}; };
#endif // KEEPASSXC_WINDOWSHELLO_H #endif // KEEPASSXC_WINDOWSHELLO_H

View file

@ -12,5 +12,10 @@
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="PolkitSubject"/> <annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="PolkitSubject"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QMap&lt;QString, QString&gt;"/> <annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QMap&lt;QString, QString&gt;"/>
</method> </method>
<method name="EnumerateActions">
<arg type="s" name="locale" direction="in" />
<arg type="a(ssssssuuua{ss})" name="action_descriptions" direction="out" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="PolkitActionDescriptionList"/>
</method>
</interface> </interface>
</node> </node>