Compare commits

...

16 Commits

Author SHA1 Message Date
Jonathan White
f15ba49fc6 WIP: Enable centralized secret storage
* Also enables pin unlock to be stored

TODO: Clean up pin unlock interface with polkit
2025-11-05 19:54:13 -05:00
Jonathan White
67b550bb6e Address PR comments 2025-11-05 19:22:52 -05:00
Jonathan White
4e2c06b943 Add safeguard to using Argon2 function 2025-11-05 19:22:51 -05:00
Jonathan White
656e0c71a3 Add Pin Quick Unlock option
* Introduce QuickUnlockManager to fall back to pin unlock if OS native options are not available.
2025-11-05 19:22:51 -05:00
Jonathan White
d2ad2a95fe Add support to remember quick unlock on Windows and macOS 2025-11-05 19:22:51 -05:00
Juzu-O
592d553ff8 Add URL double-click action option to Settings (#12322)
* Closes #4717

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: juzu-o <3142026+juzu-o@users.noreply.github.com>
Co-authored-by: Jonathan White <support@dmapps.us>
2025-11-02 12:32:11 -05:00
MNarath
a709f14cf3 Fix KeeShare entries with references not updating (#11809)
A Entry that gets shared containing a reference Attribute would not write a history entry upon resolving said Attribute resulting in the import into the target database not beeing triggered despite the changes beeing written to the keeshare db.
2025-11-02 12:29:14 -05:00
Isaac Elliott
9031cb530e Allow read-only native message files (#12236)
* Allow read-only native message files

It's possible[^1] for a native message file to be both correct and read-only.
When current versions of `keepassxc` encounter this, it fails, because
it can't write to the file. In this situation it should only fail if
the read-only file's contents are different to those it's trying to
write.

[^1]: e.g. when using an immutable OS management system like NixOS or
   home-manager.

---------

Co-authored-by: Jonathan White <support@dmapps.us>
2025-11-02 10:17:06 -05:00
Jonathan White
ebf0676661 Fix Auto-Type Empty Window Behavior
* Fixes #9282
* Also improve documentation for window title matching behavior
2025-11-02 10:16:41 -05:00
Sebastian Livoni
6130a64be5 Add Window menu for macOS and specify Help menu to AppKit (#12357)
* Add Window menu for macOS and specify Help menu to AppKit
* Fix potential NSString dangling pointers of temporary QStrings
2025-11-02 10:16:22 -05:00
renner
9814037fd3 feat: refresh appdata.xml
* Adds donation, contact and up-to-date transifex URL
* Add features to appdata.xml for FlatHub
* Remove old releases to reduce file size
* Improve summary and description text
2025-11-02 09:08:07 -05:00
renner
8c8ae49240 chore: reformat xml with GUI tool 2025-11-02 09:08:07 -05:00
Chris Bednarski
1e370b8ab8 Change StartupNotify to false
StartupNotify causes KeepassXC to hang on startup until the notification timeout is reached, making the KeepassXC window unavailable in the application switcher (i.e. alt-tab) on various Linux distros.

Fixes https://github.com/keepassxreboot/keepassxc/issues/6423
Fixes https://github.com/keepassxreboot/keepassxc/issues/11664
2025-11-02 08:58:02 -05:00
Jonathan White
cd9bb483fe Fix saving "Search Wait for Enter" setting 2025-11-02 06:24:36 -05:00
Sertonix
2cc2c905b5 Fix uninitialized memory when --pw-stdin is used with a pipe 2025-11-01 19:56:11 -04:00
Siddhant Shekhar
d9ccf767d0 Sanitize username to prevent single-instance detection failure (#12559)
---------

Co-authored-by: Jonathan White <support@dmapps.us>
2025-11-01 10:25:10 -04:00
56 changed files with 2383 additions and 2097 deletions

View File

@@ -32,7 +32,9 @@ To configure Auto-Type sequences for your entries, perform the following steps:
1. Navigate to the entries list and open the desired entry for editing. Click the _Auto-Type_ item from the left-hand menu bar *(1)*. Press the kbd:[+] button *(2)* to add a new sequence entry. Select the desired window using the drop-down menu, or simply type a window title in the box *(3)*.
+
TIP: You can use an asterisk (`\*`) to match any value (e.g., when a window title contains a dynamic filename or website name). Set the window title to `*` to match all windows. Leave the window title blank to offer additional default Auto-Type sequences, such as custom attributes.
TIP: You can use an asterisk (`\*`) as a wildcard (e.g., when a window title contains a dynamic file or website name). Set the window title to `*` to match all windows. Leave the window title blank to offer additional sequences for every matching window. This is useful for typing individual custom attributes, for example.
+
TIP: To use a standard regular expression for window title matching, the window title must start and end with two forward slashes (e.g., `//^Secure Login - .*$//`).
+
.Auto-Type entry sequences
image::autotype_entry_sequences.png[]

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ Exec=keepassxc %f
TryExec=keepassxc
Icon=@APP_ICON_NAME@
StartupWMClass=keepassxc
StartupNotify=true
StartupNotify=false
Terminal=false
Type=Application
Version=1.5

View File

@@ -157,6 +157,25 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AppKit</name>
<message>
<source>Window</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Minimize</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Zoom</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Bring All to Front</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ApplicationSettingsWidget</name>
<message>
@@ -561,10 +580,6 @@
<source>Export settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open browser on double clicking URL field in entry view</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Font size:</source>
<translation type="unfinished"></translation>
@@ -577,6 +592,26 @@
<source>Skip confirmation for main window Auto-Type actions</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Double-click action for URL:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Double-click action for URL field</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Edit entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open entry URL in browser</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy entry URL to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Auto-generate password for new entries</source>
<translation type="unfinished"></translation>
@@ -622,10 +657,6 @@
<source>Convenience</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enable database quick unlock (Touch ID / Windows Hello)</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Lock databases when session is locked or lid is closed</source>
<translation type="unfinished"></translation>
@@ -670,6 +701,14 @@
<source>Hide notes in the entry preview panel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enable database quick unlock by default</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remember quick unlock after database is closed</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AttachmentWidget</name>
@@ -1619,10 +1658,6 @@ Backup database located at %2</source>
<source>Unlock Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unlock</source>
<translation type="unfinished"></translation>
@@ -1700,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>
<translation type="unfinished"></translation>
</message>
<message>
<source>authenticate to access the database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to authenticate with Quick Unlock: %1</source>
<translation type="unfinished"></translation>
@@ -1756,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>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reset</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware keys found, but no slots are configured.</source>
<translation type="unfinished"></translation>
@@ -1764,6 +1803,10 @@ Are you sure you want to continue with this file?.</source>
<source>Press ESC again to close this database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Quick Unlock</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@@ -9091,46 +9134,10 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Passkeys</source>
<translation type="unfinished"></translation>
</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>
<source>Polkit returned an error: %1</source>
<translation type="unfinished"></translation>
</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>
<source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation>
@@ -9139,10 +9146,6 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Failed to encrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
@@ -9306,7 +9309,35 @@ This option is deprecated, use --set-key-file instead.</source>
<translation type="unfinished"></translation>
</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>
</message>
<message>
@@ -9390,6 +9421,34 @@ This option is deprecated, use --set-key-file instead.</source>
<source>Confirm Replace Entry References</source>
<translation type="unfinished"></translation>
</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>
<name>QtIOCompressor</name>

View File

@@ -217,6 +217,7 @@ set(gui_SOURCES
gui/wizard/NewDatabaseWizardPageEncryption.cpp
gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp
quickunlock/QuickUnlockInterface.cpp
quickunlock/PinUnlock.cpp
../share/icons/icons.qrc
../share/wizard/wizard.qrc)
@@ -227,40 +228,41 @@ if(APPLE)
gui/osutils/macutils/ScreenLockListenerMac.cpp
gui/osutils/macutils/AppKitImpl.mm
gui/osutils/macutils/AppKit.h
quickunlock/TouchID.mm)
# TODO: Remove -Wno-error once deprecation warnings have been resolved.
set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast")
quickunlock/TouchID.cpp)
endif()
if(UNIX AND NOT APPLE)
list(APPEND gui_SOURCES
gui/osutils/nixutils/ScreenLockListenerDBus.cpp
gui/osutils/nixutils/NixUtils.cpp)
if("${CMAKE_SYSTEM}" MATCHES "Linux")
list(APPEND core_SOURCES
quickunlock/Polkit.cpp
quickunlock/PolkitDbusTypes.cpp)
endif()
if(WITH_XC_X11)
list(APPEND gui_SOURCES
gui/osutils/nixutils/X11Funcs.cpp)
endif()
# Polkit is only available on Linux systems
if("${CMAKE_SYSTEM}" MATCHES "Linux")
list(APPEND gui_SOURCES
quickunlock/Polkit.cpp
quickunlock/PolkitDbusTypes.cpp)
set_source_files_properties(
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
PROPERTIES
INCLUDE "quickunlock/PolkitDbusTypes.h"
)
qt5_add_dbus_interface(gui_SOURCES
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
polkit_dbus
)
endif()
# dbus support
qt5_add_dbus_adaptor(gui_SOURCES
gui/org.keepassxc.KeePassXC.MainWindow.xml
gui/MainWindow.h
MainWindow)
set_source_files_properties(
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
PROPERTIES
INCLUDE "quickunlock/PolkitDbusTypes.h"
)
qt5_add_dbus_interface(core_SOURCES
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
polkit_dbus
)
find_library(KEYUTILS_LIBRARIES NAMES keyutils)
if(NOT KEYUTILS_LIBRARIES)
message(FATAL_ERROR "Could not find libkeyutils")

View File

@@ -372,16 +372,30 @@ bool NativeMessageInstaller::createNativeMessageFile(SupportedBrowsers browser)
QFile scriptFile(path);
if (!scriptFile.open(QIODevice::WriteOnly)) {
qWarning() << "Browser Plugin: Failed to open native message file for writing at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
if (!scriptFile.open(QIODevice::ReadOnly)) {
qWarning() << "Browser Plugin: Failed to open native message file at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
}
// We failed to write to `scriptFile`, but we can read it, so we assume that it's a read-only file.
// Consider success if the read-only file already contains the content we would have written.
QJsonDocument expectedDoc(constructFile(browser));
QJsonDocument actualDoc = QJsonDocument::fromJson(scriptFile.readAll());
if (expectedDoc != actualDoc) {
qWarning() << "Browser Plugin: Unexpected (read-only) native message file at " << scriptFile.fileName();
qWarning() << "Expected contents: " << expectedDoc;
return false;
}
} else {
QJsonDocument doc(constructFile(browser));
if (scriptFile.write(doc.toJson()) < 0) {
qWarning() << "Browser Plugin: Failed to write native message file at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
}
}
QJsonDocument doc(constructFile(browser));
if (scriptFile.write(doc.toJson()) < 0) {
qWarning() << "Browser Plugin: Failed to write native message file at " << scriptFile.fileName();
qWarning() << scriptFile.errorString();
return false;
}
return true;
}

View File

@@ -105,7 +105,9 @@ namespace Utils
SetConsoleMode(hIn, mode);
#else
struct termios t;
tcgetattr(STDIN_FILENO, &t);
if (tcgetattr(STDIN_FILENO, &t) < 0) {
return;
}
if (enable) {
t.c_lflag |= ECHO;

View File

@@ -67,6 +67,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::SearchLimitGroup,{QS("SearchLimitGroup"), Roaming, false}},
{Config::MinimizeOnOpenUrl,{QS("MinimizeOnOpenUrl"), Roaming, false}},
{Config::OpenURLOnDoubleClick, {QS("OpenURLOnDoubleClick"), Roaming, true}},
{Config::URLDoubleClickAction, {QS("URLDoubleClickAction"), Roaming, 0}},
{Config::HideWindowOnCopy,{QS("HideWindowOnCopy"), Roaming, false}},
{Config::MinimizeOnCopy,{QS("MinimizeOnCopy"), Roaming, true}},
{Config::AutoGeneratePasswordForNewEntries,{QS("AutoGeneratePasswordForNewEntries"), Roaming, false}},
@@ -118,6 +119,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::GUI_CheckForUpdates, {QS("GUI/CheckForUpdates"), Roaming, true}},
{Config::GUI_CheckForUpdatesNextCheck, {QS("GUI/CheckForUpdatesNextCheck"), Local, 0}},
{Config::GUI_CheckForUpdatesIncludeBetas, {QS("GUI/CheckForUpdatesIncludeBetas"), Roaming, false}},
{Config::GUI_SearchWaitForEnter, {QS("GUI/SearchWaitForEnter"), Roaming, false}},
{Config::GUI_ShowExpiredEntriesOnDatabaseUnlock, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlock"), Roaming, true}},
{Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlockOffsetDays"), Roaming, 3}},
{Config::GUI_FontSizeOffset, {QS("GUI/FontSizeOffset"), Local, 0}},
@@ -153,7 +155,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}},
{Config::Security_DatabasePasswordMinimumQuality, {QS("Security/DatabasePasswordMinimumQuality"), Local, 0}},
{Config::Security_QuickUnlockRemember, {QS("Security/QuickUnlockRemember"), Local, true}},
// Browser
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},
@@ -491,6 +493,15 @@ void Config::migrate()
remove(GUI_ListViewState);
}
// Migrate from boolean OpenURLOnDoubleClick to enum URLDoubleClickAction
if (m_settings->contains(configStrings[OpenURLOnDoubleClick].name)
&& !m_settings->contains(configStrings[URLDoubleClickAction].name)) {
bool openUrlOnDoubleClick = get(OpenURLOnDoubleClick).toBool();
// Convert: true (open browser) -> 0, false (edit entry) -> 2
set(URLDoubleClickAction, openUrlOnDoubleClick ? 0 : 2);
// Keep the old setting for now for compatibility
}
m_settings->setValue("ConfigVersion", CONFIG_VERSION);
sync();
}

View File

@@ -50,6 +50,7 @@ public:
SearchLimitGroup,
MinimizeOnOpenUrl,
OpenURLOnDoubleClick,
URLDoubleClickAction,
HideWindowOnCopy,
MinimizeOnCopy,
MinimizeAfterUnlock,
@@ -99,7 +100,7 @@ public:
GUI_CompactMode,
GUI_CheckForUpdates,
GUI_CheckForUpdatesIncludeBetas,
SearchWaitForEnter,
GUI_SearchWaitForEnter,
GUI_ShowExpiredEntriesOnDatabaseUnlock,
GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays,
GUI_FontSizeOffset,
@@ -135,6 +136,7 @@ public:
Security_NoConfirmMoveEntryToRecycleBin,
Security_EnableCopyOnDoubleClick,
Security_QuickUnlock,
Security_QuickUnlockRemember,
Security_DatabasePasswordMinimumQuality,
Browser_Enabled,

View File

@@ -334,12 +334,15 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
};
QList<QString> sequenceList;
QList<QString> emptyWindowSequences;
// Add window association matches
const auto assocList = autoTypeAssociations()->getAll();
for (const auto& assoc : assocList) {
auto window = resolveMultiplePlaceholders(assoc.window);
if (!assoc.window.isEmpty() && windowMatches(window)) {
if (assoc.window.isEmpty()) {
emptyWindowSequences << assoc.sequence;
} else if (windowMatches(window)) {
if (!assoc.sequence.isEmpty()) {
sequenceList << assoc.sequence;
} else {
@@ -358,6 +361,11 @@ QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
sequenceList << effectiveAutoTypeSequence();
}
// If any associations were made, include the empty window associations
if (!sequenceList.isEmpty()) {
sequenceList.append(emptyWindowSequences);
}
return sequenceList;
}

View File

@@ -426,6 +426,29 @@ namespace Tools
return filename.trimmed();
}
QString cleanUsername()
{
#if defined(Q_OS_WIN)
QString userName = qgetenv("USERNAME");
if (userName.isEmpty()) {
userName = qgetenv("USER");
}
#else
QString userName = qgetenv("USER");
if (userName.isEmpty()) {
userName = qgetenv("USERNAME");
}
#endif
// Sanitize username for file safety
userName = userName.trimmed();
// Replace <>:"/\|?* with _
userName.replace(QRegularExpression(R"([<>:\"\/\\|?*])"), "_");
// Remove trailing dots and spaces
userName.replace(QRegularExpression(R"([.\s]+$)"), "");
return userName;
}
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties)
{
QVariantMap result;

View File

@@ -48,6 +48,7 @@ namespace Tools
QString envSubstitute(const QString& filepath,
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment());
QString cleanFilename(QString filename);
QString cleanUsername();
template <class T> QSet<T> asSet(const QList<T>& a)
{

View File

@@ -163,6 +163,13 @@ QVariantMap Argon2Kdf::writeParameters()
bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const
{
// This is a programming error and will result in broken encryption
Q_ASSERT(*raw != *result);
if (*raw == *result) {
qWarning("Argon2Kdf: Input and output buffers must not be the same.");
return false;
}
result.clear();
result.resize(32);
// Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length

View File

@@ -20,6 +20,7 @@
#include "Application.h"
#include "core/Bootstrap.h"
#include "core/Tools.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
@@ -31,6 +32,7 @@
#include <QLocalSocket>
#include <QLockFile>
#include <QPixmapCache>
#include <QRegularExpression>
#include <QSocketNotifier>
#include <QStandardPaths>
@@ -63,20 +65,19 @@ Application::Application(int& argc, char** argv)
registerUnixSignals();
#endif
QString userName = qgetenv("USER");
if (userName.isEmpty()) {
userName = qgetenv("USERNAME");
}
QString identifier = "keepassxc";
if (!userName.isEmpty()) {
identifier += "-" + userName;
// Build identifier
auto identifier = QStringLiteral("keepassxc");
auto username = Tools::cleanUsername();
if (!username.isEmpty()) {
identifier += QChar('-') + username;
}
#ifdef QT_DEBUG
// In DEBUG mode don't interfere with Release instances
identifier += "-DEBUG";
// In DEBUG mode dont interfere with Release instances
identifier += QStringLiteral("-DEBUG");
#endif
QString lockName = identifier + ".lock";
m_socketName = identifier + ".socket";
QString lockName = identifier + QStringLiteral(".lock");
m_socketName = identifier + QStringLiteral(".socket");
// According to documentation we should use RuntimeLocation on *nixes, but even Qt doesn't respect
// this and creates sockets in TempLocation, so let's be consistent.

View File

@@ -211,7 +211,7 @@ void ApplicationSettingsWidget::loadSettings()
m_generalUi->autoReloadOnChangeCheckBox->setChecked(config()->get(Config::AutoReloadOnChange).toBool());
m_generalUi->minimizeAfterUnlockCheckBox->setChecked(config()->get(Config::MinimizeAfterUnlock).toBool());
m_generalUi->minimizeOnOpenUrlCheckBox->setChecked(config()->get(Config::MinimizeOnOpenUrl).toBool());
m_generalUi->openUrlOnDoubleClick->setChecked(config()->get(Config::OpenURLOnDoubleClick).toBool());
m_generalUi->urlDoubleClickComboBox->setCurrentIndex(config()->get(Config::URLDoubleClickAction).toInt());
m_generalUi->hideWindowOnCopyCheckBox->setChecked(config()->get(Config::HideWindowOnCopy).toBool());
hideWindowOnCopyCheckBoxToggled(m_generalUi->hideWindowOnCopyCheckBox->isChecked());
m_generalUi->minimizeOnCopyRadioButton->setChecked(config()->get(Config::MinimizeOnCopy).toBool());
@@ -348,8 +348,15 @@ void ApplicationSettingsWidget::loadSettings()
m_secUi->hideTotpCheckBox->setChecked(config()->get(Config::Security_HideTotpPreviewPanel).toBool());
m_secUi->hideNotesCheckBox->setChecked(config()->get(Config::Security_HideNotes).toBool());
m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable());
m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
m_secUi->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool());
#ifdef Q_OS_LINUX
// Remembering quick unlock is not supported on Linux
m_secUi->quickUnlockRememberCheckBox->setVisible(false);
#else
// Only show this option if Touch ID or Windows Hello are available for use
m_secUi->quickUnlockRememberCheckBox->setVisible(getQuickUnlock()->isNativeAvailable());
#endif
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings();
@@ -389,7 +396,7 @@ void ApplicationSettingsWidget::saveSettings()
config()->set(Config::AutoReloadOnChange, m_generalUi->autoReloadOnChangeCheckBox->isChecked());
config()->set(Config::MinimizeAfterUnlock, m_generalUi->minimizeAfterUnlockCheckBox->isChecked());
config()->set(Config::MinimizeOnOpenUrl, m_generalUi->minimizeOnOpenUrlCheckBox->isChecked());
config()->set(Config::OpenURLOnDoubleClick, m_generalUi->openUrlOnDoubleClick->isChecked());
config()->set(Config::URLDoubleClickAction, m_generalUi->urlDoubleClickComboBox->currentIndex());
config()->set(Config::HideWindowOnCopy, m_generalUi->hideWindowOnCopyCheckBox->isChecked());
config()->set(Config::MinimizeOnCopy, m_generalUi->minimizeOnCopyRadioButton->isChecked());
config()->set(Config::DropToBackgroundOnCopy, m_generalUi->dropToBackgroundOnCopyRadioButton->isChecked());
@@ -469,9 +476,8 @@ void ApplicationSettingsWidget::saveSettings()
config()->set(Config::Security_HideTotpPreviewPanel, m_secUi->hideTotpCheckBox->isChecked());
config()->set(Config::Security_HideNotes, m_secUi->hideNotesCheckBox->isChecked());
if (m_secUi->quickUnlockCheckBox->isEnabled()) {
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
}
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked());
// Security: clear storage if related settings are disabled
if (!config()->get(Config::RememberLastDatabases).toBool()) {

View File

@@ -148,7 +148,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -174,7 +174,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -210,7 +210,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -250,7 +250,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -315,7 +315,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -483,7 +483,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -516,7 +516,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -554,14 +554,59 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="openUrlOnDoubleClick">
<property name="text">
<string>Open browser on double clicking URL field in entry view</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
<layout class="QHBoxLayout" name="urlDoubleClickLayout">
<item>
<widget class="QLabel" name="urlDoubleClickLabel">
<property name="text">
<string>Double-click action for URL:</string>
</property>
<property name="buddy">
<cstring>urlDoubleClickComboBox</cstring>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="urlDoubleClickComboBox">
<property name="focusPolicy">
<enum>Qt::StrongFocus</enum>
</property>
<property name="accessibleName">
<string>Double-click action for URL field</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
<item>
<property name="text">
<string>Open entry URL in browser</string>
</property>
</item>
<item>
<property name="text">
<string>Copy entry URL to clipboard</string>
</property>
</item>
<item>
<property name="text">
<string>Edit entry</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="urlDoubleClickSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="useGroupIconOnEntryCreationCheckBox">
@@ -603,7 +648,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -636,7 +681,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -698,7 +743,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -926,7 +971,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -974,7 +1019,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -1031,7 +1076,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -1439,7 +1484,7 @@
<tabstop>alternativeSaveComboBox</tabstop>
<tabstop>ConfirmMoveEntryToRecycleBinCheckBox</tabstop>
<tabstop>EnableCopyOnDoubleClickCheckBox</tabstop>
<tabstop>openUrlOnDoubleClick</tabstop>
<tabstop>urlDoubleClickComboBox</tabstop>
<tabstop>useGroupIconOnEntryCreationCheckBox</tabstop>
<tabstop>minimizeOnOpenUrlCheckBox</tabstop>
<tabstop>hideWindowOnCopyCheckBox</tabstop>

View File

@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>364</width>
<height>505</height>
<width>437</width>
<height>529</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -138,7 +138,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -168,7 +168,14 @@
<item>
<widget class="QCheckBox" name="quickUnlockCheckBox">
<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>
</widget>
</item>

View File

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

View File

@@ -38,14 +38,6 @@
namespace
{
constexpr int clearFormsDelay = 30000;
bool isQuickUnlockAvailable()
{
if (config()->get(Config::Security_QuickUnlock).toBool()) {
return getQuickUnlock()->isAvailable();
}
return false;
}
} // namespace
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
@@ -68,17 +60,10 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->editPassword->setShowPassword(false);
});
QFont font;
font.setPointSize(font.pointSize() + 4);
font.setBold(true);
m_ui->labelHeadline->setFont(font);
m_ui->quickUnlockButton->setFont(font);
m_ui->quickUnlockButton->setIcon(
icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText)));
m_ui->quickUnlockButton->setIconSize({32, 32});
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
QFont largeFont;
largeFont.setPointSize(largeFont.pointSize() + 4);
largeFont.setBold(true);
m_ui->labelHeadline->setFont(largeFont);
auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok);
okBtn->setText(tr("Unlock"));
@@ -86,16 +71,19 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
// Key file components
m_ui->selectKeyFileComponent->setVisible(false);
connect(m_ui->addKeyFileLinkLabel, &QLabel::linkActivated, this, &DatabaseOpenWidget::browseKeyFile);
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) {
bool state = !text.isEmpty();
m_ui->addKeyFileLinkLabel->setVisible(!state);
m_ui->selectKeyFileComponent->setVisible(state);
});
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
m_ui->selectKeyFileComponent->setVisible(false);
// Hardware key components
toggleHardwareKeyComponent(false);
connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled);
QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy();
sp.setRetainSizeWhenHidden(true);
@@ -127,13 +115,24 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->refreshHardwareKeys->setVisible(false);
#endif
// QuickUnlock actions
// QuickUnlock components
m_ui->quickUnlockButton->setFont(largeFont);
m_ui->quickUnlockButton->setIcon(
icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText)));
connect(m_ui->quickUnlockButton, &QPushButton::pressed, this, [this] { openDatabase(); });
connect(m_ui->resetQuickUnlockButton, &QPushButton::pressed, this, [this] { resetQuickUnlock(); });
connect(m_ui->closeQuickUnlockButton, &QPushButton::pressed, this, [this] { reject(); });
m_ui->resetQuickUnlockButton->setShortcut(Qt::Key_Escape);
}
DatabaseOpenWidget::~DatabaseOpenWidget() = default;
DatabaseOpenWidget::~DatabaseOpenWidget()
{
// Reset quick unlock if we are not remembering it
if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) {
resetQuickUnlock();
}
}
void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state)
{
@@ -189,7 +188,7 @@ bool DatabaseOpenWidget::event(QEvent* event)
auto type = event->type();
if (type == QEvent::Show || type == QEvent::WindowActivate) {
if (isOnQuickUnlockScreen() && (m_db.isNull() || !canPerformQuickUnlock())) {
if (isOnQuickUnlockScreen() && !canPerformQuickUnlock()) {
resetQuickUnlock();
}
toggleQuickUnlockScreen();
@@ -294,6 +293,7 @@ void DatabaseOpenWidget::load(const QString& filename)
}
toggleQuickUnlockScreen();
m_ui->enableQuickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
#ifdef WITH_XC_YUBIKEY
// Do initial auto-poll
@@ -335,16 +335,12 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
m_ui->editPassword->setText(pw);
m_ui->keyFileLineEdit->setText(keyFile);
m_blockQuickUnlock = true;
m_ui->enableQuickUnlockCheckBox->setChecked(false);
openDatabase();
}
void DatabaseOpenWidget::openDatabase()
{
// Cache this variable for future use then reset
bool blockQuickUnlock = m_blockQuickUnlock || isOnQuickUnlockScreen();
m_blockQuickUnlock = false;
setUserInteractionLock(true);
m_ui->editPassword->setShowPassword(false);
m_ui->messageWidget->hide();
@@ -386,10 +382,13 @@ void DatabaseOpenWidget::openDatabase()
}
}
// Save Quick Unlock credentials if available
if (!blockQuickUnlock && isQuickUnlockAvailable()) {
// Save Quick Unlock credentials if available and enabled
if (!isOnQuickUnlockScreen() && isQuickUnlockAvailable() && m_ui->enableQuickUnlockCheckBox->isChecked()) {
auto keyData = databaseKey->serialize();
getQuickUnlock()->setKey(m_db->publicUuid(), keyData);
auto qu = getQuickUnlock()->interface();
if (!qu->setKey(m_db->publicUuid(), keyData) && !qu->errorString().isEmpty()) {
getMainWindow()->displayTabMessage(qu->errorString(), MessageWidget::MessageType::Warning);
}
m_ui->messageWidget->hideMessage();
}
@@ -434,13 +433,16 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
{
auto databaseKey = QSharedPointer<CompositeKey>::create();
if (!m_db.isNull() && canPerformQuickUnlock()) {
// try to retrieve the stored password using Windows Hello
if (canPerformQuickUnlock()) {
// try to retrieve the stored password using quick unlock
QByteArray keyData;
if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) {
m_ui->messageWidget->showMessage(
tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()),
MessageWidget::Error);
auto qu = getQuickUnlock()->interface();
if (!qu->getKey(m_db->publicUuid(), keyData)) {
m_ui->messageWidget->showMessage(tr("Failed to authenticate with Quick Unlock: %1").arg(qu->errorString()),
MessageWidget::Error);
if (!qu->hasKey(m_db->publicUuid())) {
resetQuickUnlock();
}
return {};
}
databaseKey->setRawKey(keyData);
@@ -627,9 +629,15 @@ void DatabaseOpenWidget::setUserInteractionLock(bool state)
m_unlockingDatabase = state;
}
bool DatabaseOpenWidget::isQuickUnlockAvailable() const
{
auto qu = getQuickUnlock()->interface();
return qu && qu->isAvailable();
}
bool DatabaseOpenWidget::canPerformQuickUnlock() const
{
return !m_db.isNull() && isQuickUnlockAvailable() && getQuickUnlock()->hasKey(m_db->publicUuid());
return m_db && isQuickUnlockAvailable() && getQuickUnlock()->interface()->hasKey(m_db->publicUuid());
}
bool DatabaseOpenWidget::isOnQuickUnlockScreen() const
@@ -656,7 +664,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen()
void DatabaseOpenWidget::triggerQuickUnlock()
{
if (isOnQuickUnlockScreen()) {
if (isOnQuickUnlockScreen() && !unlockingDatabase()) {
m_ui->quickUnlockButton->click();
}
}
@@ -668,11 +676,9 @@ void DatabaseOpenWidget::triggerQuickUnlock()
*/
void DatabaseOpenWidget::resetQuickUnlock()
{
if (!isQuickUnlockAvailable()) {
return;
}
if (!m_db.isNull()) {
getQuickUnlock()->reset(m_db->publicUuid());
auto qu = getQuickUnlock()->interface();
if (m_db && qu) {
qu->reset(m_db->publicUuid());
}
load(m_filename);
}

View File

@@ -19,7 +19,6 @@
#ifndef KEEPASSX_DATABASEOPENWIDGET_H
#define KEEPASSX_DATABASEOPENWIDGET_H
#include <QPointer>
#include <QScopedPointer>
#include <QTimer>
@@ -46,21 +45,17 @@ class DatabaseOpenWidget : public DialogyWidget
public:
explicit DatabaseOpenWidget(QWidget* parent = nullptr);
~DatabaseOpenWidget() override;
void load(const QString& filename);
QString filename();
QSharedPointer<Database> database();
void clearForms();
void enterKey(const QString& pw, const QString& keyFile);
QSharedPointer<Database> database();
void triggerQuickUnlock();
bool unlockingDatabase();
void showMessage(const QString& text, MessageWidget::MessageType type, int autoHideTimeout);
// Quick Unlock helper functions
bool canPerformQuickUnlock() const;
bool isOnQuickUnlockScreen() const;
void toggleQuickUnlockScreen();
void triggerQuickUnlock();
void resetQuickUnlock();
signals:
void dialogFinished(bool accepted);
@@ -85,14 +80,20 @@ private slots:
void closeDatabase();
void pollHardwareKey(bool manualTrigger = false, int delay = 0);
void hardwareKeyResponse(bool found);
void resetQuickUnlock();
private:
// Quick Unlock helper functions
bool isQuickUnlockAvailable() const;
bool canPerformQuickUnlock() const;
bool isOnQuickUnlockScreen() const;
void toggleQuickUnlockScreen();
#ifdef WITH_XC_YUBIKEY
QPointer<DeviceListener> m_deviceListener;
#endif
bool m_pollingHardwareKey = false;
bool m_manualHardwareKeyRefresh = false;
bool m_blockQuickUnlock = false;
bool m_unlockingDatabase = false;
bool m_triedToQuit = false;
QTimer m_hideTimer;

View File

@@ -180,7 +180,7 @@
<number>0</number>
</property>
<property name="bottomMargin">
<number>10</number>
<number>0</number>
</property>
<item>
<widget class="QLabel" name="passwordLabel">
@@ -192,7 +192,7 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>2</number>
<number>0</number>
</property>
<item>
<widget class="PasswordWidget" name="editPassword" native="true">
@@ -250,13 +250,13 @@
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
<number>10</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>10</number>
<number>15</number>
</property>
<item>
<widget class="QLabel" name="selectKeyFileLabel">
@@ -399,7 +399,7 @@
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
<height>0</height>
</size>
</property>
</spacer>
@@ -465,6 +465,48 @@
<property name="bottomMargin">
<number>5</number>
</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">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="focusPolicy">
@@ -511,6 +553,9 @@
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
@@ -542,17 +587,81 @@
<property name="text">
<string>Unlock Database</string>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetQuickUnlockButton">
<property name="text">
<string>Cancel</string>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</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>
<spacer name="verticalSpacer_8">
@@ -645,8 +754,6 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>editPassword</tabstop>
<tabstop>keyFileLineEdit</tabstop>
<tabstop>buttonBrowseFile</tabstop>
@@ -654,7 +761,11 @@
<tabstop>hardwareKeyCombo</tabstop>
<tabstop>refreshHardwareKeys</tabstop>
<tabstop>addKeyFileLinkLabel</tabstop>
<tabstop>enableQuickUnlockCheckBox</tabstop>
<tabstop>buttonBox</tabstop>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
<tabstop>closeQuickUnlockButton</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@@ -1583,12 +1583,21 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod
// case EntryModel::Attachments:
// break;
case EntryModel::Url:
if (!entry->url().isEmpty() && config()->get(Config::OpenURLOnDoubleClick).toBool()) {
openUrlForEntry(entry);
break;
if (!entry->url().isEmpty()) {
switch (config()->get(Config::URLDoubleClickAction).toInt()) {
case 2: // Edit entry
switchToEntryEdit(entry);
break;
case 1: // Copy entry URL to clipboard
setClipboardTextAndMinimize(entry->resolveMultiplePlaceholders(entry->url()));
break;
case 0: // Open entry URL in browser (default)
default:
openUrlForEntry(entry);
break;
}
}
// Note, order matters here. We want to fall into the default case.
[[fallthrough]];
break;
default:
switchToEntryEdit(entry);
}
@@ -1969,8 +1978,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event)
event->ignore();
return;
}
m_databaseOpenWidget->resetQuickUnlock();
event->accept();
}

View File

@@ -93,6 +93,10 @@ MainWindow::MainWindow()
m_ui->setupUi(this);
#ifdef Q_OS_MACOS
macUtils()->configureWindowAndHelpMenus(this, m_ui->menuHelp);
#endif
#if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS)
new MainWindowAdaptor(this);
QDBusConnection dbus = QDBusConnection::sessionBus();

View File

@@ -72,10 +72,10 @@ SearchWidget::SearchWidget(QWidget* parent)
m_actionLimitGroup->setChecked(config()->get(Config::SearchLimitGroup).toBool());
m_actionWaitForEnter = m_searchMenu->addAction(
tr("Press Enter to search"), this, [](bool state) { config()->set(Config::SearchWaitForEnter, state); });
tr("Press Enter to search"), this, [](bool state) { config()->set(Config::GUI_SearchWaitForEnter, state); });
m_actionWaitForEnter->setObjectName("actionSearchWaitForEnter");
m_actionWaitForEnter->setCheckable(true);
m_actionWaitForEnter->setChecked(config()->get(Config::SearchWaitForEnter).toBool());
m_actionWaitForEnter->setChecked(config()->get(Config::GUI_SearchWaitForEnter).toBool());
m_ui->searchIcon->setIcon(icons()->icon("system-search"));
m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition);

View File

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

View File

@@ -72,6 +72,14 @@ public:
virtual bool canPreventScreenCapture() const = 0;
virtual bool setPreventScreenCapture(QWindow* window, bool allow) const;
/**
* Platform specific secrets storage/handling
*/
virtual bool saveSecret(const QString& key, const QByteArray& secretData) const = 0;
virtual bool getSecret(const QString& key, QByteArray& secretData) const = 0;
virtual bool removeSecret(const QString& key) const = 0;
virtual bool removeAllSecrets() const = 0;
signals:
void globalShortcutTriggered(const QString& name, const QString& search = {});

View File

@@ -21,6 +21,8 @@
#include <QColor>
#include <QObject>
#include <QMenu>
#include <QMainWindow>
#include <unistd.h>
class QWindow;
@@ -45,6 +47,7 @@ public:
bool enableScreenRecording();
void toggleForegroundApp(bool foreground);
void setWindowSecurity(QWindow* window, bool state);
void configureWindowAndHelpMenus(QMainWindow* mainWindow, QMenu* helpMenu);
signals:
void userSwitched();

View File

@@ -42,5 +42,6 @@
- (bool) enableScreenRecording;
- (void) toggleForegroundApp:(bool) foreground;
- (void) setWindowSecurity:(NSWindow*) window state:(bool) state;
- (void) configureWindowAndHelpMenus:(QMainWindow*) mainWindow helpMenu:(QMenu*) helpMenu;
@end

View File

@@ -17,12 +17,22 @@
*/
#import "AppKitImpl.h"
#import "MacUtils.h"
#import <QWindow>
#import <QMenu>
#import <QMenuBar>
#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
#import <ScreenCaptureKit/ScreenCaptureKit.h>
#endif
#include "config-keepassx.h"
@implementation AppKitImpl
- (id) initWithObject:(AppKit*)appkit
@@ -184,7 +194,7 @@
//
// Check if screen recording is enabled, may show an popup asking for permissions
//
- (bool) enableScreenRecording
- (bool) enableScreenRecording
{
#if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3
if (@available(macOS 12.3, *)) {
@@ -192,7 +202,7 @@
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
// Attempt to use SCShareableContent to check for screen recording permission
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content,
[SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent * _Nullable content,
NSError * _Nullable error) {
Q_UNUSED(error);
if (content) {
@@ -231,8 +241,29 @@
[window setSharingType: state ? NSWindowSharingNone : NSWindowSharingReadOnly];
}
- (void) configureWindowAndHelpMenus:(QMainWindow*) mainWindow helpMenu:(QMenu*) helpMenu
{
QMenu *qtWindowMenu = new QMenu(AppKit::tr("Window"));
NSMenu *nsWindowMenu = qtWindowMenu->toNSMenu();
QString minimizeStr = AppKit::tr("Minimize");
[nsWindowMenu addItemWithTitle:minimizeStr.toNSString() action:@selector(performMiniaturize:) keyEquivalent:@""];
QString zoomStr = AppKit::tr("Zoom");
[nsWindowMenu addItemWithTitle:zoomStr.toNSString() action:@selector(performZoom:) keyEquivalent:@""];
[nsWindowMenu addItem:[NSMenuItem separatorItem]];
QString bringAllToFrontStr = AppKit::tr("Bring All to Front");
[nsWindowMenu addItemWithTitle:bringAllToFrontStr.toNSString() action:@selector(arrangeInFront:) keyEquivalent:@""];
NSApp.windowsMenu = nsWindowMenu;
mainWindow->menuBar()->insertMenu(helpMenu->menuAction(), qtWindowMenu);
NSApp.helpMenu = helpMenu->toNSMenu();
}
@end
//
// ------------------------- C++ Trampolines -------------------------
//
@@ -312,3 +343,227 @@ void AppKit::setWindowSecurity(QWindow* window, bool state)
auto view = reinterpret_cast<NSView*>(window->winId());
[static_cast<id>(self) setWindowSecurity:view.window state:state];
}
void AppKit::configureWindowAndHelpMenus(QMainWindow* window, QMenu* 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

@@ -24,6 +24,7 @@
#include <QStandardPaths>
#include <QTimer>
#include <QWindow>
#include <QMenu>
#include <ApplicationServices/ApplicationServices.h>
@@ -202,6 +203,11 @@ void MacUtils::registerNativeEventFilter()
::InstallApplicationEventHandler(MacUtils::hotkeyHandler, 1, &eventSpec, this, nullptr);
}
void MacUtils::configureWindowAndHelpMenus(QMainWindow* mainWindow, QMenu* helpMenu)
{
return m_appkit->configureWindowAndHelpMenus(mainWindow, helpMenu);
}
bool MacUtils::registerGlobalShortcut(const QString& name, Qt::Key key, Qt::KeyboardModifiers modifiers, QString* error)
{
auto keycode = qtToNativeKeyCode(key);

View File

@@ -54,6 +54,8 @@ public:
void registerNativeEventFilter() override;
void configureWindowAndHelpMenus(QMainWindow* mainWindow, QMenu* helpMenu);
bool registerGlobalShortcut(const QString& name,
Qt::Key key,
Qt::KeyboardModifiers modifiers,
@@ -66,6 +68,21 @@ public:
bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
// Key management API (TouchID)
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;
enum class AuthPolicy
{
TouchId,
Watch,
PasswordFallback
};
bool isAuthPolicyAvailable(AuthPolicy policy) const;
signals:
void userSwitched();

View File

@@ -30,6 +30,11 @@
#include <QStandardPaths>
#include <QStyle>
#include <QTextStream>
extern "C" {
#include <keyutils.h>
}
#ifdef WITH_XC_X11
#include <QX11Info>
@@ -156,7 +161,7 @@ void NixUtils::setLaunchAtStartup(bool enable)
<< QStringLiteral("TryExec=") << executeablePathOrName << '\n'
<< QStringLiteral("Icon=") << QApplication::applicationName().toLower() << '\n'
<< QStringLiteral("StartupWMClass=keepassxc") << '\n'
<< QStringLiteral("StartupNotify=true") << '\n'
<< QStringLiteral("StartupNotify=false") << '\n'
<< QStringLiteral("Terminal=false") << '\n'
<< QStringLiteral("Type=Application") << '\n'
<< QStringLiteral("Version=1.0") << '\n'
@@ -411,3 +416,74 @@ quint64 NixUtils::getProcessStartTime() const
qDebug() << "nixutils: failed to find ')' in " << processStatPath;
return 0;
}
namespace
{
key_serial_t getKeyring()
{
auto keyring = keyctl_get_persistent(-1, KEY_SPEC_PROCESS_KEYRING);
if (keyring == -1) {
// Return the non-persistent keyring as a fallback
qWarning("nixutils: failed to get persistent keyring: %s", strerror(errno));
keyring = KEY_SPEC_PROCESS_KEYRING;
}
return keyring;
}
} // namespace
bool NixUtils::saveSecret(const QString& key, const QByteArray& secretData) const
{
auto keyserial =
add_key("user", key.toStdString().c_str(), secretData.constData(), secretData.size(), getKeyring());
if (keyserial < 0) {
qWarning("nixutils: failed to save secret: %s", strerror(errno));
return false;
}
// Only allow this process to read/write this key
keyctl_setperm(keyserial, KEY_POS_ALL);
return true;
}
bool NixUtils::getSecret(const QString& key, QByteArray& secretData) const
{
secretData.clear();
auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
if (keyserial < 0) {
qWarning("nixutils: failed to find secret: %s", strerror(errno));
return false;
}
secretData.resize(512);
auto size = keyctl_read(keyserial, secretData.data(), secretData.size());
if (size == -1) {
qWarning("nixutils: failed to read secret: %s", strerror(errno));
return false;
}
secretData.resize(size);
return true;
}
bool NixUtils::removeSecret(const QString& key) const
{
auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring());
if (keyserial < 0) {
qWarning("nixutils: failed to find secret: %s", strerror(errno));
return false;
}
if (keyctl_unlink(keyserial, getKeyring()) < 0) {
qWarning("nixutils: failed to remove secret: %s", strerror(errno));
return false;
}
return true;
}
bool NixUtils::removeAllSecrets() const
{
// NixUtils does not support clearing all keys
return false;
}

View File

@@ -52,6 +52,11 @@ public:
quint64 getProcessStartTime() const;
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;
private slots:
void handleColorSchemeRead(QDBusVariant value);
void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value);

View File

@@ -20,11 +20,24 @@
#include <QApplication>
#include <QDir>
#include <QSettings>
#include <QUuid>
#include <QWindow>
#include <Windows.h>
#include <winrt/base.h>
#include <winrt/windows.foundation.collections.h>
#include <winrt/windows.security.credentials.h>
#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;
WinUtils* WinUtils::instance()
@@ -361,3 +374,59 @@ DWORD WinUtils::qtToNativeModifiers(Qt::KeyboardModifiers modifiers)
return nativeModifiers;
}
bool WinUtils::saveSecret(const QString& key, const QByteArray& secretData) const
{
try {
auto vault = PasswordVault();
vault.Add({s_winKeyStoreName,
winrt::hstring(key.toStdWString()),
winrt::to_hstring(secretData.toBase64().toStdString())});
return true;
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to add key to password vault");
return false;
}
}
bool WinUtils::getSecret(const QString& key, QByteArray& secretData) const
{
secretData.clear();
try {
auto vault = PasswordVault();
auto credential = vault.Retrieve(s_winKeyStoreName, winrt::hstring(key.toStdWString()));
secretData = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password())));
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to retrieve key from password vault");
return false;
}
return !secretData.isEmpty();
}
bool WinUtils::removeSecret(const QString& key) const
{
try {
auto vault = PasswordVault();
vault.Remove({s_winKeyStoreName, winrt::hstring(key.toStdWString()), L"nodata"});
return true;
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to clear key from password vault");
return false;
}
}
bool WinUtils::removeAllSecrets() const
{
auto vault = PasswordVault();
auto credentials = vault.FindAllByResource(s_winKeyStoreName);
bool allSuccess = true;
for (const auto& credential : credentials) {
try {
vault.Remove(credential);
} catch (winrt::hresult_error const&) {
qWarning("WinUtils - Failed to clear key from password vault");
allSuccess = false;
}
}
return allSuccess;
}

View File

@@ -61,6 +61,11 @@ public:
bool canPreventScreenCapture() const override;
bool setPreventScreenCapture(QWindow* window, bool prevent) const override;
bool saveSecret(const QString& key, const QByteArray& secretData) const override;
bool getSecret(const QString& key, QByteArray& secretData) const override;
bool removeSecret(const QString& key) const override;
bool removeAllSecrets() const override;
protected:
explicit WinUtils(QObject* parent = nullptr);
~WinUtils() override = default;

View File

@@ -56,9 +56,9 @@ namespace
// but those cases with high probability constructed examples and very rare in real usage
const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid());
const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue);
targetEntry->setUpdateTimeinfo(false);
targetEntry->beginUpdate();
targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute));
targetEntry->setUpdateTimeinfo(true);
targetEntry->endUpdate();
}
}

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
* it under the terms of the GNU General Public License as published by
@@ -23,8 +23,8 @@
#include "gui/osutils/nixutils/NixUtils.h"
#include <QDebug>
#include <QFile>
#include <QtDBus>
#include <botan/mem_ops.h>
#include <cerrno>
@@ -35,19 +35,11 @@ extern "C" {
const QString polkit_service = "org.freedesktop.PolicyKit1";
const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority";
namespace
{
QString getKeyName(const QUuid& dbUuid)
{
static const QString keyPrefix = "keepassxc_polkit_keys_";
return keyPrefix + dbUuid.toString();
}
} // namespace
Polkit::Polkit()
{
PolkitSubject::registerMetaType();
PolkitAuthorizationResults::registerMetaType();
PolkitActionDescription::registerMetaType();
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden
through an environment variable to return an alternative bus path. This bus could have an application
@@ -61,18 +53,34 @@ Polkit::Polkit()
m_available = bus.isConnected();
if (!m_available) {
qDebug() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
qWarning() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
return;
}
m_available = bus.interface()->isServiceRegistered(polkit_service);
if (!m_available) {
qDebug() << "polkit: Polkit is not registered on dbus";
qWarning() << "polkit: Polkit is not registered on dbus";
return;
}
// Initiate the Polkit dbus interface
m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus));
// Reset available state and check Polkit registered actions for KeePassXC
m_available = false;
auto kpxcAction = QStringLiteral("org.keepassxc.KeePassXC.unlockDatabase");
auto actions = m_polkit->EnumerateActions("");
for (const auto& action : actions.value()) {
if (action.actionId == kpxcAction) {
m_available = true;
break;
}
}
if (!m_available) {
qWarning() << "polkit: KeePassXC Polkit action is not installed";
}
}
Polkit::~Polkit()
@@ -81,7 +89,8 @@ Polkit::~Polkit()
void Polkit::reset(const QUuid& dbUuid)
{
m_encryptedMasterKeys.remove(dbUuid);
m_sessionKeys.remove(dbUuid);
nixUtils()->removeSecret(dbUuid.toString());
}
bool Polkit::isAvailable() const
@@ -89,67 +98,100 @@ bool Polkit::isAvailable() const
return m_available;
}
QString Polkit::errorString() const
{
return m_error;
}
void Polkit::reset()
{
m_encryptedMasterKeys.clear();
m_sessionKeys.clear();
nixUtils()->removeAllSecrets();
}
bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key)
bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& data)
{
reset(dbUuid);
// Generate a random iv/key pair to encrypt the master password with
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
QByteArray keychainKeyValue = randomKey + randomIV;
// Prompt for a pin to use as session key
QByteArray key;
if (!promptPin(0, key)) {
return false;
}
auto iv = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
m_error = QObject::tr("AES initialization failed");
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
// Encrypt the master password
QByteArray encryptedMasterKey = key;
if (!aes256Encrypt.finish(encryptedMasterKey)) {
m_error = QObject::tr("AES encrypt failed");
qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
// Encrypt the database key
QByteArray encrypted = data;
if (!aes256Encrypt.finish(encrypted)) {
m_error = QObject::tr("Failed to encrypt key data.");
return false;
}
// Add the iv/key pair into the linux keyring
key_serial_t key_serial = add_key("user",
getKeyName(dbUuid).toStdString().c_str(),
keychainKeyValue.constData(),
keychainKeyValue.size(),
KEY_SPEC_PROCESS_KEYRING);
if (key_serial < 0) {
m_error = QObject::tr("Failed to store in Linux Keyring");
qDebug() << "polkit keyring failed to store: " << errno;
return false;
}
// Store the session key and save the encrypted master key to the keyring
m_sessionKeys.insert(dbUuid, key);
nixUtils()->saveSecret(dbUuid.toString(), encrypted.prepend(iv));
// Scrub the keys from ram
Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
Botan::secure_scrub_memory(keychainKeyValue.data(), keychainKeyValue.size());
// Store encrypted master password and return
m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
return true;
}
bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
bool Polkit::getKey(const QUuid& dbUuid, QByteArray& data)
{
if (!m_polkit || !hasKey(dbUuid)) {
if (!m_available || !hasKey(dbUuid)) {
m_error = QObject::tr("No key is stored for this database.");
return false;
}
QByteArray key;
for (int pinAttempts = 1; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
if (!m_sessionKeys.contains(dbUuid)) {
// Request pin to obtain a session key
if (!promptPin(pinAttempts, key)) {
m_error = QObject::tr("Failed to obtain session key.");
return false;
}
} else {
// We already have the session key, prompt using polkit to authorize use
if (!promptPolkit()) {
// Error set in promptPolkit call
return false;
}
key = m_sessionKeys.value(dbUuid);
}
// Retrieve the encrypted master key from the OS secret store
QByteArray encData;
if (!nixUtils()->getSecret(dbUuid.toString(), encData)) {
m_error = QObject::tr("Failed to get credentials for quick unlock.");
return false;
}
const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& iv = encData.left(ivSize);
// Decrypt the data using the generated key and IV from above
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
// Attempt to decrypt the key data
data = encData.mid(ivSize);
if (cipher.finish(data)) {
// Decryption succeeded, store the session key used
m_sessionKeys.insert(dbUuid, key);
return true;
}
}
m_error = QObject::tr("Too many pin attempts.");
return false;
}
bool Polkit::promptPolkit()
{
PolkitSubject subject;
subject.kind = "unix-process";
subject.details.insert("pid", static_cast<uint>(QCoreApplication::applicationPid()));
@@ -170,78 +212,26 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
if (result.isError()) {
auto msg = result.error().message();
m_error = QObject::tr("Polkit returned an error: %1").arg(msg);
qDebug() << "polkit returned an error: " << msg;
return false;
}
PolkitAuthorizationResults authResult = result.value();
if (authResult.is_authorized) {
QByteArray encryptedMasterKey = m_encryptedMasterKeys.value(dbUuid);
key_serial_t keySerial =
find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING);
if (keySerial == -1) {
m_error = QObject::tr("Could not locate key in keyring");
qDebug() << "polkit keyring failed to find: " << errno;
return false;
}
void* keychainBuffer;
long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer);
if (keychainDataSize == -1) {
m_error = QObject::tr("Could not read key in keyring");
qDebug() << "polkit keyring failed to read: " << errno;
return false;
}
QByteArray keychainBytes(static_cast<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;
}
// Failed to authenticate
if (authResult.is_challenge) {
m_error = QObject::tr("No Polkit authentication agent was available");
m_error = QObject::tr("No Polkit authentication agent was available.");
} else {
m_error = QObject::tr("Polkit authorization failed");
m_error = QObject::tr("Polkit authorization failed.");
}
return false;
}
bool Polkit::hasKey(const QUuid& dbUuid) const
{
if (!m_encryptedMasterKeys.contains(dbUuid)) {
return false;
}
return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1;
// Check if the OS has a secret stored for this database UUID
QByteArray tmp;
return nixUtils()->getSecret(dbUuid.toString(), tmp);
}

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
* 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/>.
*/
#ifndef KEEPASSX_POLKIT_H
#define KEEPASSX_POLKIT_H
#pragma once
#include "QuickUnlockInterface.h"
#include "PinUnlock.h"
#include "polkit_dbus.h"
#include <QHash>
#include <QScopedPointer>
class Polkit : public QuickUnlockInterface
class Polkit : public PinUnlock
{
public:
Polkit();
~Polkit() override;
bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool setKey(const QUuid& dbUuid, const QByteArray& data) override;
bool getKey(const QUuid& dbUuid, QByteArray& data) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
bool promptPolkit();
bool m_available;
QString m_error;
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
QHash<QUuid, QByteArray> m_sessionKeys;
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"
void PolkitSubject::registerMetaType()
@@ -43,3 +60,32 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizati
argument.endStructure();
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>
@@ -30,7 +46,30 @@ public:
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject);
};
class PolkitActionDescription
{
public:
QString actionId;
QString description;
QString message;
QString vendorName;
QString vendorUrl;
QString iconName;
uint implicitAny;
uint implicitInactive;
uint implicitActive;
QMap<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(PolkitAuthorizationResults);
#endif // KEEPASSX_POLKITDBUSTYPES_H
Q_DECLARE_METATYPE(PolkitActionDescription);
Q_DECLARE_METATYPE(PolkitActionDescriptionList);

View File

@@ -16,66 +16,55 @@
*/
#include "QuickUnlockInterface.h"
#include "PinUnlock.h"
#include <QObject>
#if defined(Q_OS_MACOS)
#include "TouchID.h"
#define QUICKUNLOCK_IMPLEMENTATION TouchID
#elif defined(Q_CC_MSVC)
#include "WindowsHello.h"
#define QUICKUNLOCK_IMPLEMENTATION WindowsHello
#elif defined(Q_OS_LINUX)
#include "Polkit.h"
#define QUICKUNLOCK_IMPLEMENTATION Polkit
#else
#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock
#endif
QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr};
QuickUnlockManager* g_quickUnlockManager = nullptr;
QuickUnlockInterface* getQuickUnlock()
QuickUnlockManager* getQuickUnlock()
{
if (!quickUnlockInstance) {
quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION();
if (!g_quickUnlockManager) {
g_quickUnlockManager = new QuickUnlockManager();
}
return quickUnlockInstance;
return g_quickUnlockManager;
}
bool NoQuickUnlock::isAvailable() const
QuickUnlockManager::QuickUnlockManager()
{
return false;
// Create the native interface based on the platform
#if defined(Q_OS_MACOS)
m_nativeInterface.reset(new TouchID());
#elif defined(Q_CC_MSVC)
m_nativeInterface.reset(new WindowsHello());
#elif defined(Q_OS_LINUX)
m_nativeInterface.reset(new Polkit());
#endif
// Always create the fallback interface
m_fallbackInterface.reset(new PinUnlock());
}
QString NoQuickUnlock::errorString() const
{
return QObject::tr("No Quick Unlock provider is available");
}
void NoQuickUnlock::reset()
QuickUnlockManager::~QuickUnlockManager()
{
}
bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key)
QSharedPointer<QuickUnlockInterface> QuickUnlockManager::interface() const
{
Q_UNUSED(dbUuid)
Q_UNUSED(key)
return false;
if (isNativeAvailable()) {
return m_nativeInterface;
}
return m_fallbackInterface;
}
bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key)
bool QuickUnlockManager::isNativeAvailable() const
{
Q_UNUSED(dbUuid)
Q_UNUSED(key)
return false;
}
bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const
{
Q_UNUSED(dbUuid)
return false;
}
void NoQuickUnlock::reset(const QUuid& dbUuid)
{
Q_UNUSED(dbUuid)
return m_nativeInterface && m_nativeInterface->isAvailable();
}

View File

@@ -18,6 +18,7 @@
#ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H
#define KEEPASSXC_QUICKUNLOCKINTERFACE_H
#include <QSharedPointer>
#include <QUuid>
class QuickUnlockInterface
@@ -29,7 +30,6 @@ public:
virtual ~QuickUnlockInterface() = default;
virtual bool isAvailable() const = 0;
virtual QString errorString() const = 0;
virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0;
virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0;
@@ -37,22 +37,32 @@ public:
virtual void reset(const QUuid& dbUuid) = 0;
virtual void reset() = 0;
virtual QString errorString() const
{
return m_error;
}
protected:
QString m_error;
};
class NoQuickUnlock : public QuickUnlockInterface
class QuickUnlockManager final
{
Q_DISABLE_COPY(QuickUnlockManager)
public:
bool isAvailable() const override;
QString errorString() const override;
QuickUnlockManager();
~QuickUnlockManager();
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
QSharedPointer<QuickUnlockInterface> interface() const;
bool isNativeAvailable() const;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
QSharedPointer<QuickUnlockInterface> m_nativeInterface;
QSharedPointer<QuickUnlockInterface> m_fallbackInterface;
};
QuickUnlockInterface* getQuickUnlock();
QuickUnlockManager* getQuickUnlock();
#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/>.
*/
#ifndef KEEPASSX_TOUCHID_H
#define KEEPASSX_TOUCHID_H
#pragma once
#include "QuickUnlockInterface.h"
#include <QHash>
class TouchID : public QuickUnlockInterface
{
public:
bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override;
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
@@ -33,17 +30,4 @@ public:
void reset(const QUuid& dbUuid = "") override;
void reset() override;
private:
static bool isWatchAvailable();
static bool isTouchIdAvailable();
static bool isPasswordFallbackPossible();
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID);
static void deleteKeyEntry(const QString& accountName);
static QString databaseKeyName(const QUuid& dbUuid);
QHash<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 <Userconsentverifierinterop.h>
#include <Windows.h>
#include <winrt/base.h>
#include <winrt/windows.foundation.collections.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.security.credentials.h>
#include <winrt/windows.security.cryptography.h>
@@ -28,12 +29,14 @@
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include "gui/osutils/OSUtils.h"
#include <QTimer>
#include <QWindow>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Security::Credentials;
using namespace Windows::Security::Cryptography;
using namespace Windows::Storage::Streams;
@@ -43,17 +46,20 @@ namespace
const std::wstring s_winHelloKeyName{L"keepassxc_winhello"};
int g_promptFocusCount = 0;
void queueSecurityPromptFocus(int delay = 500)
void queueSecurityPromptFocus(bool initial, int delay = 500)
{
if (initial) {
g_promptFocusCount = 0;
}
QTimer::singleShot(delay, [] {
auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr);
if (hWnd) {
::SetForegroundWindow(hWnd);
} else if (++g_promptFocusCount <= 3) {
queueSecurityPromptFocus();
return;
qDebug("WindowsHello - Could not find security prompt window");
queueSecurityPromptFocus(false);
}
g_promptFocusCount = 0;
});
}
@@ -105,14 +111,9 @@ bool WindowsHello::isAvailable() const
return task.get();
}
QString WindowsHello::errorString() const
{
return m_error;
}
bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
{
queueSecurityPromptFocus();
queueSecurityPromptFocus(true);
// Generate a random challenge that will be signed by Windows Hello
// to create the key. The challenge is also used as the IV.
@@ -120,6 +121,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
auto challenge = Random::instance()->randomArray(ivSize);
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
m_error = QObject::tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled.");
return false;
}
@@ -137,28 +139,28 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
// Prepend the challenge/IV to the encrypted data
encrypted.prepend(challenge);
m_encryptedKeys.insert(dbUuid, encrypted);
return true;
return osUtils->saveSecret(dbUuid.toString(), encrypted);
}
bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
{
data.clear();
if (!hasKey(dbUuid)) {
m_error = QObject::tr("Failed to get Windows Hello credential.");
QByteArray keydata;
if (!osUtils->getSecret(dbUuid.toString(), keydata)) {
m_error = QObject::tr("Failed to retrieve Windows Hello credential.");
return false;
}
queueSecurityPromptFocus();
queueSecurityPromptFocus(true);
// Read the previously used challenge and encrypted data
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& keydata = m_encryptedKeys.value(dbUuid);
auto challenge = keydata.left(ivSize);
auto encrypted = keydata.mid(ivSize);
QByteArray key;
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
// Error is set in deriveEncryptionKey
return false;
}
@@ -182,15 +184,16 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
void WindowsHello::reset(const QUuid& dbUuid)
{
m_encryptedKeys.remove(dbUuid);
osUtils->removeSecret(dbUuid.toString());
}
bool WindowsHello::hasKey(const QUuid& dbUuid) const
{
return m_encryptedKeys.contains(dbUuid);
QByteArray tmp;
return osUtils->getSecret(dbUuid.toString(), tmp);
}
void WindowsHello::reset()
{
m_encryptedKeys.clear();
osUtils->removeAllSecrets();
}

View File

@@ -20,26 +20,22 @@
#include "QuickUnlockInterface.h"
#include <QHash>
#include <QObject>
class WindowsHello : public QuickUnlockInterface
{
public:
WindowsHello() = default;
bool isAvailable() const override;
QString errorString() const override;
void reset() override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
QString m_error;
QHash<QUuid, QByteArray> m_encryptedKeys;
Q_DISABLE_COPY(WindowsHello);
Q_DISABLE_COPY(WindowsHello)
};
#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.In2" value="QMap&lt;QString, QString&gt;"/>
</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>
</node>

View File

@@ -125,6 +125,20 @@ void TestAutoType::init()
m_entry5->setPassword("example5");
m_entry5->setTitle("some title");
m_entry5->setUrl("http://example.org");
m_entry6 = new Entry();
m_entry6->setGroup(m_group);
m_entry6->setPassword("example6");
m_entry6->setTitle("empty window test");
association.window = "";
association.sequence = "{S:Empty Window}";
m_entry6->autoTypeAssociations()->add(association);
association.window = "non-matching window";
association.sequence = "should not match";
m_entry6->autoTypeAssociations()->add(association);
association.window = "*notepad*";
association.sequence = "{USERNAME}";
m_entry6->autoTypeAssociations()->add(association);
}
void TestAutoType::cleanup()
@@ -446,3 +460,13 @@ void TestAutoType::testAutoTypeEffectiveSequences()
QCOMPARE(entry6->defaultAutoTypeSequence(), sequenceOrphan);
QCOMPARE(entry6->effectiveAutoTypeSequence(), QString());
}
void TestAutoType::testAutoTypeEmptyWindowAssociation()
{
auto assoc = m_entry6->autoTypeSequences("Windows Notepad");
QCOMPARE(assoc.size(), 2);
QVERIFY(assoc.contains("{S:Empty Window}"));
assoc = m_entry6->autoTypeSequences("Some Other Window");
QVERIFY(assoc.isEmpty());
}

View File

@@ -51,6 +51,7 @@ private slots:
void testAutoTypeResults_data();
void testAutoTypeSyntaxChecks();
void testAutoTypeEffectiveSequences();
void testAutoTypeEmptyWindowAssociation();
private:
AutoTypePlatformInterface* m_platform;
@@ -64,6 +65,7 @@ private:
Entry* m_entry3;
Entry* m_entry4;
Entry* m_entry5;
Entry* m_entry6;
};
#endif // KEEPASSX_TESTAUTOTYPE_H

View File

@@ -17,6 +17,7 @@
#include "TestConfig.h"
#include <QSettings>
#include <QTest>
#include "config-keepassx-tests.h"
@@ -40,3 +41,39 @@ void TestConfig::testUpgrade()
tempFile.remove();
}
void TestConfig::testURLDoubleClickMigration()
{
// Test migration from OpenURLOnDoubleClick to URLDoubleClickAction
TemporaryFile tempFile;
tempFile.open();
// Create a config with old setting = true (open browser)
QSettings oldConfig(tempFile.fileName(), QSettings::IniFormat);
oldConfig.setValue("OpenURLOnDoubleClick", true);
oldConfig.sync();
tempFile.close();
Config::createConfigFromFile(tempFile.fileName());
// Should migrate to URLDoubleClickAction = 0 (open browser)
QCOMPARE(config()->get(Config::URLDoubleClickAction).toInt(), 0);
tempFile.remove();
// Test migration from OpenURLOnDoubleClick = false (edit entry)
TemporaryFile tempFile2;
tempFile2.open();
QSettings oldConfig2(tempFile2.fileName(), QSettings::IniFormat);
oldConfig2.setValue("OpenURLOnDoubleClick", false);
oldConfig2.sync();
tempFile2.close();
Config::createConfigFromFile(tempFile2.fileName());
// Should migrate to URLDoubleClickAction = 2 (edit entry)
QCOMPARE(config()->get(Config::URLDoubleClickAction).toInt(), 2);
tempFile2.remove();
}

View File

@@ -25,6 +25,7 @@ class TestConfig : public QObject
Q_OBJECT
private slots:
void testUpgrade();
void testURLDoubleClickMigration();
};
#endif // KEEPASSX_TESTCONFIG_H

View File

@@ -428,3 +428,26 @@ void TestTools::testIsTextMimeType()
QVERIFY(!Tools::isTextMimeType(noText));
}
}
// Test sanitization logic for Tools::cleanUsername
void TestTools::testCleanUsername()
{
// Test vars
QFETCH(QString, input);
QFETCH(QString, expected);
qputenv("USER", input.toUtf8());
qputenv("USERNAME", input.toUtf8());
QCOMPARE(Tools::cleanUsername(), expected);
}
void TestTools::testCleanUsername_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("expected");
QTest::newRow("Leading and trailing spaces") << " user " << "user";
QTest::newRow("Special characters") << R"(user<>:"/\|?*name)" << "user_________name";
QTest::newRow("Trailing dots and spaces") << "username... " << "username";
QTest::newRow("Combination of issues") << R"( user<>:"/\|?*name... )" << "user_________name";
}

View File

@@ -41,6 +41,8 @@ private slots:
void testGetMimeType();
void testGetMimeTypeByFileInfo();
void testIsTextMimeType();
void testCleanUsername();
void testCleanUsername_data();
};
#endif // KEEPASSX_TESTTOOLS_H