mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-12-04 15:39:34 +01:00
Compare commits
16 Commits
f927c4c41a
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f15ba49fc6 | ||
|
|
67b550bb6e | ||
|
|
4e2c06b943 | ||
|
|
656e0c71a3 | ||
|
|
d2ad2a95fe | ||
|
|
592d553ff8 | ||
|
|
a709f14cf3 | ||
|
|
9031cb530e | ||
|
|
ebf0676661 | ||
|
|
6130a64be5 | ||
|
|
9814037fd3 | ||
|
|
8c8ae49240 | ||
|
|
1e370b8ab8 | ||
|
|
cd9bb483fe | ||
|
|
2cc2c905b5 | ||
|
|
d9ccf767d0 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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 "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><a href="#" style="text-decoration: underline">I have a key file</a></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 'xml', 'csv' or 'html'. Defaults to 'xml'.</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 'xml', 'csv' or 'html'. Defaults to 'xml'.</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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 don’t 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.
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
207
src/quickunlock/PinUnlock.cpp
Normal file
207
src/quickunlock/PinUnlock.cpp
Normal 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();
|
||||
}
|
||||
54
src/quickunlock/PinUnlock.h
Normal file
54
src/quickunlock/PinUnlock.h
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
72
src/quickunlock/TouchID.cpp
Normal file
72
src/quickunlock/TouchID.cpp
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,5 +12,10 @@
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="PolkitSubject"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QMap<QString, QString>"/>
|
||||
</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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class TestConfig : public QObject
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void testUpgrade();
|
||||
void testURLDoubleClickMigration();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTCONFIG_H
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -41,6 +41,8 @@ private slots:
|
||||
void testGetMimeType();
|
||||
void testGetMimeTypeByFileInfo();
|
||||
void testIsTextMimeType();
|
||||
void testCleanUsername();
|
||||
void testCleanUsername_data();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTTOOLS_H
|
||||
|
||||
Reference in New Issue
Block a user