Add support for Windows Hello

* Special thanks to @HexF and @smlu for their contributions towards this feature.

* Add MVP support for Windows Hello as a Quick Unlock solution using the WinRT API. This works by signing a random challenge vector with the Windows Hello protected key store (typically from TPM). The signed challenge is hashed using SHA-256 and then used as the encryption key to encrypt the database credentials. Credentials are encrypted using AES-256/GCM. This ensures the database password can only be decrypted following a successful authentication with Windows Hello in the future.

* Unify Touch ID and Windows Hello behavior under the Quick Unlock branding. Remove all timeout features of Touch ID as they are unnecessary and complicate the feature for no security gain.

* Quick Unlock is automatically reset only when the database key is changed vice whenever database settings are modified.

* Don't set database unlock dialog as always on top. This allows Touch ID and Windows Hello prompts to appear above the dialog properly.

* Prevent quick unlock when using AutoOpen or opening from the command line.
This commit is contained in:
Jonathan White 2022-02-21 20:40:01 -05:00
parent a76daeb4c5
commit 4f0710350f
33 changed files with 1058 additions and 734 deletions

View File

@ -58,9 +58,6 @@ option(WITH_XC_UPDATECHECK "Include automatic update checks; disable for control
if(UNIX AND NOT APPLE)
option(WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API." OFF)
endif()
if(APPLE)
option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF)
endif()
option(WITH_XC_DOCS "Enable building of documentation" ON)
if(WITH_CCACHE)
@ -81,9 +78,6 @@ if(WITH_XC_ALL)
set(WITH_XC_YUBIKEY ON)
set(WITH_XC_SSHAGENT ON)
set(WITH_XC_KEESHARE ON)
if(APPLE)
set(WITH_XC_TOUCHID ON)
endif()
if(UNIX AND NOT APPLE)
set(WITH_XC_FDOSECRETS ON)
endif()

View File

@ -141,7 +141,7 @@ Files: share/icons/badges/2_Expired.svg
share/icons/database/C46_Help.svg
share/icons/database/C53_Apply.svg
share/icons/database/C61_Services.svg
Copyright: 2020 KeePassXC Team <team@keepassxc.org>
Copyright: 2022 KeePassXC Team <team@keepassxc.org>
License: MIT
Files: share/icons/application/scalable/actions/chevron-double-down.svg
@ -166,6 +166,7 @@ Files: share/icons/application/scalable/actions/chevron-double-down.svg
share/icons/application/scalable/actions/entry-edit.svg
share/icons/application/scalable/actions/entry-new.svg
share/icons/application/scalable/actions/favicon-download.svg
share/icons/application/scalable/actions/fingerprint.svg
share/icons/application/scalable/actions/group-clone.svg
share/icons/application/scalable/actions/group-delete.svg
share/icons/application/scalable/actions/group-edit.svg

View File

@ -99,7 +99,6 @@ These steps place the compiled KeePassXC binary inside the `./build/src/` direct
-DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF)
-DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF)
-DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF)
-DWITH_XC_TOUCHID=[ON|OFF] (macOS Only) Enable/Disable Touch ID unlock (default:OFF)
-DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF)
-DWITH_XC_KEESHARE=[ON|OFF] Enable/Disable KeeShare group synchronization extension (default: OFF)
-DWITH_XC_ALL=[ON|OFF] Enable/Disable compiling all plugins above (default: OFF)

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17.81,4.47C17.73,4.47 17.65,4.45 17.58,4.41C15.66,3.42 14,3 12,3C10.03,3 8.15,3.47 6.44,4.41C6.2,4.54 5.9,4.45 5.76,4.21C5.63,3.97 5.72,3.66 5.96,3.53C7.82,2.5 9.86,2 12,2C14.14,2 16,2.47 18.04,3.5C18.29,3.65 18.38,3.95 18.25,4.19C18.16,4.37 18,4.47 17.81,4.47M3.5,9.72C3.4,9.72 3.3,9.69 3.21,9.63C3,9.47 2.93,9.16 3.09,8.93C4.08,7.53 5.34,6.43 6.84,5.66C10,4.04 14,4.03 17.15,5.65C18.65,6.42 19.91,7.5 20.9,8.9C21.06,9.12 21,9.44 20.78,9.6C20.55,9.76 20.24,9.71 20.08,9.5C19.18,8.22 18.04,7.23 16.69,6.54C13.82,5.07 10.15,5.07 7.29,6.55C5.93,7.25 4.79,8.25 3.89,9.5C3.81,9.65 3.66,9.72 3.5,9.72M9.75,21.79C9.62,21.79 9.5,21.74 9.4,21.64C8.53,20.77 8.06,20.21 7.39,19C6.7,17.77 6.34,16.27 6.34,14.66C6.34,11.69 8.88,9.27 12,9.27C15.12,9.27 17.66,11.69 17.66,14.66A0.5,0.5 0 0,1 17.16,15.16A0.5,0.5 0 0,1 16.66,14.66C16.66,12.24 14.57,10.27 12,10.27C9.43,10.27 7.34,12.24 7.34,14.66C7.34,16.1 7.66,17.43 8.27,18.5C8.91,19.66 9.35,20.15 10.12,20.93C10.31,21.13 10.31,21.44 10.12,21.64C10,21.74 9.88,21.79 9.75,21.79M16.92,19.94C15.73,19.94 14.68,19.64 13.82,19.05C12.33,18.04 11.44,16.4 11.44,14.66A0.5,0.5 0 0,1 11.94,14.16A0.5,0.5 0 0,1 12.44,14.66C12.44,16.07 13.16,17.4 14.38,18.22C15.09,18.7 15.92,18.93 16.92,18.93C17.16,18.93 17.56,18.9 17.96,18.83C18.23,18.78 18.5,18.96 18.54,19.24C18.59,19.5 18.41,19.77 18.13,19.82C17.56,19.93 17.06,19.94 16.92,19.94M14.91,22C14.87,22 14.82,22 14.78,22C13.19,21.54 12.15,20.95 11.06,19.88C9.66,18.5 8.89,16.64 8.89,14.66C8.89,13.04 10.27,11.72 11.97,11.72C13.67,11.72 15.05,13.04 15.05,14.66C15.05,15.73 16,16.6 17.13,16.6C18.28,16.6 19.21,15.73 19.21,14.66C19.21,10.89 15.96,7.83 11.96,7.83C9.12,7.83 6.5,9.41 5.35,11.86C4.96,12.67 4.76,13.62 4.76,14.66C4.76,15.44 4.83,16.67 5.43,18.27C5.53,18.53 5.4,18.82 5.14,18.91C4.88,19 4.59,18.87 4.5,18.62C4,17.31 3.77,16 3.77,14.66C3.77,13.46 4,12.37 4.45,11.42C5.78,8.63 8.73,6.82 11.96,6.82C16.5,6.82 20.21,10.33 20.21,14.65C20.21,16.27 18.83,17.59 17.13,17.59C15.43,17.59 14.05,16.27 14.05,14.65C14.05,13.58 13.12,12.71 11.97,12.71C10.82,12.71 9.89,13.58 9.89,14.65C9.89,16.36 10.55,17.96 11.76,19.16C12.71,20.1 13.62,20.62 15.03,21C15.3,21.08 15.45,21.36 15.38,21.62C15.33,21.85 15.12,22 14.91,22Z" /></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -39,6 +39,7 @@
<file>application/scalable/actions/entry-edit.svg</file>
<file>application/scalable/actions/entry-new.svg</file>
<file>application/scalable/actions/favicon-download.svg</file>
<file>application/scalable/actions/fingerprint.svg</file>
<file>application/scalable/actions/getting-started.svg</file>
<file>application/scalable/actions/group-delete.svg</file>
<file>application/scalable/actions/group-edit.svg</file>
@ -80,7 +81,6 @@
<file>application/scalable/actions/username-copy.svg</file>
<file>application/scalable/actions/view-history.svg</file>
<file>application/scalable/actions/web.svg</file>
<file>application/scalable/apps/freedesktop.svg</file>
<file>application/scalable/apps/internet-web-browser.svg</file>
<file>application/scalable/apps/keepassxc.svg</file>

View File

@ -508,14 +508,6 @@
<source>Lock databases after inactivity of</source>
<translation>Lock databases after inactivity of</translation>
</message>
<message>
<source> min</source>
<translation> min</translation>
</message>
<message>
<source>Forget TouchID after inactivity of</source>
<translation>Forget TouchID after inactivity of</translation>
</message>
<message>
<source>Convenience</source>
<translation>Convenience</translation>
@ -524,10 +516,6 @@
<source>Lock databases when session is locked or lid is closed</source>
<translation>Lock databases when session is locked or lid is closed</translation>
</message>
<message>
<source>Forget TouchID when session is locked or lid is closed</source>
<translation>Forget TouchID when session is locked or lid is closed</translation>
</message>
<message>
<source>Lock databases after minimizing the window</source>
<translation>Lock databases after minimizing the window</translation>
@ -552,10 +540,6 @@
<source>Clipboard clear seconds</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Touch ID inactivity reset</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Database lock timeout seconds</source>
<translation type="unfinished"></translation>
@ -589,6 +573,10 @@
<source>Enable double click to copy the username/password entry columns</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Enable database quick unlock (Touch ID / Windows Hello)</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>AutoType</name>
@ -1474,10 +1462,6 @@ Backup database located at %2</source>
<source>Hardware key help</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TouchID for Quick Unlock</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unlock failed and no password given</source>
<translation type="unfinished"></translation>
@ -1501,10 +1485,6 @@ To prevent this error from appearing, you must go to &quot;Database Settings / S
<source>Key file help</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cannot use database file as key file</source>
<translation type="unfinished"></translation>
@ -1577,6 +1557,26 @@ We recommend you update your KeePassXC installation.</source>
<source>Database unlock canceled.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unlock</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to authenticate with Windows Hello</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Unlock Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Cancel</source>
<translation type="unfinished">Cancel</translation>
</message>
<message>
<source>Failed to authenticate with Touch ID</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DatabaseSettingWidgetMetaData</name>
@ -6837,10 +6837,6 @@ Kernel: %3 %4</source>
<source>YubiKey</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>TouchID</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>None</source>
<translation type="unfinished"></translation>
@ -7770,6 +7766,18 @@ Please consider generating a new key file.</source>
<source>Browser Statistics</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Quick Unlock</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to create Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to sign challenge using Windows Hello.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>
@ -8762,6 +8770,25 @@ Example: JBSWY3DPEHPK3PXP</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>WindowsHello</name>
<message>
<source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation>
</message>
<message>
<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>
</message>
</context>
<context>
<name>YubiKey</name>
<message>

View File

@ -216,6 +216,9 @@ if(WIN32)
${keepassx_SOURCES}
gui/osutils/winutils/ScreenLockListenerWin.cpp
gui/osutils/winutils/WinUtils.cpp)
if (MSVC)
list(APPEND keepassx_SOURCES winhello/WindowsHello.cpp)
endif()
endif()
set(keepassx_SOURCES ${keepassx_SOURCES}
@ -234,9 +237,6 @@ add_feature_info(UpdateCheck WITH_XC_UPDATECHECK "Automatic update checking")
if(UNIX AND NOT APPLE)
add_feature_info(FdoSecrets WITH_XC_FDOSECRETS "Implement freedesktop.org Secret Storage Spec server side API.")
endif()
if(APPLE)
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
endif()
add_subdirectory(browser)
add_subdirectory(proxy)
@ -308,7 +308,7 @@ if(WITH_XC_NETWORKING)
updatecheck/UpdateChecker.cpp)
endif()
if(WITH_XC_TOUCHID)
if(APPLE)
list(APPEND keepassx_SOURCES touchid/TouchID.mm)
# TODO: Remove -Wno-error once deprecation warnings have been resolved.
set_source_files_properties(touchid/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast -Wno-error")
@ -347,13 +347,10 @@ if(WITH_XC_KEESHARE)
endif()
if(APPLE)
target_link_libraries(keepassx_core "-framework Foundation -framework AppKit -framework Carbon")
target_link_libraries(keepassx_core "-framework Foundation -framework AppKit -framework Carbon -framework Security -framework LocalAuthentication")
if(Qt5MacExtras_FOUND)
target_link_libraries(keepassx_core Qt5::MacExtras)
endif()
if(WITH_XC_TOUCHID)
target_link_libraries(keepassx_core "-framework Security -framework LocalAuthentication")
endif()
endif()
if(HAIKU)
target_link_libraries(keepassx_core network)
@ -364,6 +361,9 @@ if(UNIX AND NOT APPLE)
endif()
if(WIN32)
target_link_libraries(keepassx_core Wtsapi32.lib Ws2_32.lib)
if (MSVC)
target_link_libraries(keepassx_core WindowsApp.lib)
endif()
endif()
if(WIN32)
@ -388,12 +388,8 @@ if(APPLE AND WITH_APP_BUNDLE)
configure_file(${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
set_target_properties(${PROGNAME} PROPERTIES
MACOSX_BUNDLE ON
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
if(WITH_XC_TOUCHID)
set_target_properties(${PROGNAME} PROPERTIES
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
endif()
if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib")
install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib"

View File

@ -19,7 +19,6 @@
#cmakedefine WITH_XC_SSHAGENT
#cmakedefine WITH_XC_KEESHARE
#cmakedefine WITH_XC_UPDATECHECK
#cmakedefine WITH_XC_TOUCHID
#cmakedefine WITH_XC_FDOSECRETS
#cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@"

View File

@ -81,7 +81,6 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::GlobalAutoTypeRetypeTime,{QS("GlobalAutoTypeRetypeTime"), Roaming, 15}},
{Config::FaviconDownloadTimeout,{QS("FaviconDownloadTimeout"), Roaming, 10}},
{Config::UpdateCheckMessageShown,{QS("UpdateCheckMessageShown"), Roaming, false}},
{Config::UseTouchID,{QS("UseTouchID"), Roaming, false}},
{Config::LastDatabases, {QS("LastDatabases"), Local, {}}},
{Config::LastKeyFiles, {QS("LastKeyFiles"), Local, {}}},
@ -140,11 +139,9 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_HidePasswordPreviewPanel, {QS("Security/HidePasswordPreviewPanel"), Roaming, true}},
{Config::Security_AutoTypeAsk, {QS("Security/AutotypeAsk"), Roaming, true}},
{Config::Security_IconDownloadFallback, {QS("Security/IconDownloadFallback"), Roaming, false}},
{Config::Security_ResetTouchId, {QS("Security/ResetTouchId"), Roaming, false}},
{Config::Security_ResetTouchIdTimeout, {QS("Security/ResetTouchIdTimeout"), Roaming, 30}},
{Config::Security_ResetTouchIdScreenlock,{QS("Security/ResetTouchIdScreenlock"), Roaming, true}},
{Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}},
{Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}},
{Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}},
// Browser
{Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}},
@ -329,9 +326,6 @@ static const QHash<QString, Config::ConfigKey> deprecationMap = {
{QS("security/HidePasswordPreviewPanel"), Config::Security_HidePasswordPreviewPanel},
{QS("security/passwordsrepeat"), Config::Security_PasswordsRepeatVisible},
{QS("security/hidenotes"), Config::Security_HideNotes},
{QS("security/resettouchid"), Config::Security_ResetTouchId},
{QS("security/resettouchidtimeout"), Config::Security_ResetTouchIdTimeout},
{QS("security/resettouchidscreenlock"), Config::Security_ResetTouchIdScreenlock},
{QS("KeeShare/Settings.own"), Config::KeeShare_Own},
{QS("KeeShare/Settings.foreign"), Config::KeeShare_Foreign},
{QS("KeeShare/Settings.active"), Config::KeeShare_Active},
@ -369,7 +363,11 @@ static const QHash<QString, Config::ConfigKey> deprecationMap = {
{QS("LastAttachmentDir"), Config::Deleted},
{QS("KeeShare/LastDir"), Config::Deleted},
{QS("KeeShare/LastKeyDir"), Config::Deleted},
{QS("KeeShare/LastShareDir"), Config::Deleted}};
{QS("KeeShare/LastShareDir"), Config::Deleted},
{QS("UseTouchID"), Config::Deleted},
{QS("Security/ResetTouchId"), Config::Deleted},
{QS("Security/ResetTouchIdTimeout"), Config::Deleted},
{QS("Security/ResetTouchIdScreenlock"), Config::Deleted}};
/**
* Migrate settings from previous versions.

View File

@ -63,7 +63,6 @@ public:
GlobalAutoTypeRetypeTime,
FaviconDownloadTimeout,
UpdateCheckMessageShown,
UseTouchID,
LastDatabases,
LastKeyFiles,
@ -120,11 +119,9 @@ public:
Security_HidePasswordPreviewPanel,
Security_AutoTypeAsk,
Security_IconDownloadFallback,
Security_ResetTouchId,
Security_ResetTouchIdTimeout,
Security_ResetTouchIdScreenlock,
Security_NoConfirmMoveEntryToRecycleBin,
Security_EnableCopyOnDoubleClick,
Security_QuickUnlock,
Browser_Enabled,
Browser_ShowNotification,

View File

@ -99,8 +99,8 @@ namespace Tools
#ifdef WITH_XC_YUBIKEY
extensions += "\n- " + QObject::tr("YubiKey");
#endif
#ifdef WITH_XC_TOUCHID
extensions += "\n- " + QObject::tr("TouchID");
#if defined(Q_OS_MACOS) || defined(Q_CC_MSVC)
extensions += "\n- " + QObject::tr("Quick Unlock");
#endif
#ifdef WITH_XC_FDOSECRETS
extensions += "\n- " + QObject::tr("Secret Service Integration");

View File

@ -176,6 +176,8 @@ SymmetricCipher::Mode SymmetricCipher::stringToMode(const QString& cipher)
return Aes128_CTR;
} else if (cipher.compare("aes-256-ctr", cs) == 0 || cipher.compare("aes256-ctr", cs) == 0) {
return Aes256_CTR;
} else if (cipher.compare("aes-256-gcm", cs) == 0 || cipher.compare("aes256-gcm", cs) == 0) {
return Aes256_GCM;
} else if (cipher.startsWith("twofish", cs)) {
return Twofish_CBC;
} else if (cipher.startsWith("salsa", cs)) {
@ -198,6 +200,8 @@ QString SymmetricCipher::modeToString(const Mode mode)
return QStringLiteral("CTR(AES-128)");
case Aes256_CTR:
return QStringLiteral("CTR(AES-256)");
case Aes256_GCM:
return QStringLiteral("AES-256/GCM");
case Twofish_CBC:
return QStringLiteral("Twofish/CBC");
case Salsa20:
@ -217,6 +221,7 @@ int SymmetricCipher::defaultIvSize(Mode mode)
case Aes256_CBC:
case Aes128_CTR:
case Aes256_CTR:
case Aes256_GCM:
case Twofish_CBC:
return 16;
case Salsa20:
@ -235,6 +240,7 @@ int SymmetricCipher::keySize(Mode mode)
return 16;
case Aes256_CBC:
case Aes256_CTR:
case Aes256_GCM:
case Twofish_CBC:
case Salsa20:
case ChaCha20:
@ -249,6 +255,7 @@ int SymmetricCipher::blockSize(Mode mode)
switch (mode) {
case Aes128_CBC:
case Aes256_CBC:
case Aes256_GCM:
case Twofish_CBC:
return 16;
case Aes128_CTR:

View File

@ -39,6 +39,7 @@ public:
Twofish_CBC,
ChaCha20,
Salsa20,
Aes256_GCM,
InvalidMode = -1,
};

View File

@ -35,6 +35,9 @@
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
#ifdef Q_CC_MSVC
#include "winhello/WindowsHello.h"
#endif
class ApplicationSettingsWidget::ExtraPage
{
@ -129,8 +132,6 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
m_secUi->clearSearchSpinBox, SLOT(setEnabled(bool)));
connect(m_secUi->lockDatabaseIdleCheckBox, SIGNAL(toggled(bool)),
m_secUi->lockDatabaseIdleSpinBox, SLOT(setEnabled(bool)));
connect(m_secUi->touchIDResetCheckBox, SIGNAL(toggled(bool)),
m_secUi->touchIDResetSpinBox, SLOT(setEnabled(bool)));
// clang-format on
// Disable mouse wheel grab when scrolling
@ -155,16 +156,14 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent)
m_generalUi->faviconTimeoutSpinBox->setVisible(false);
#endif
#ifndef WITH_XC_TOUCHID
bool hideTouchID = true;
#else
bool hideTouchID = !TouchID::getInstance().isAvailable();
bool showQuickUnlock = false;
#if defined(Q_OS_MACOS)
showQuickUnlock = TouchID::getInstance().isAvailable();
#elif defined(Q_CC_MSVC)
showQuickUnlock = getWindowsHello()->isAvailable();
connect(getWindowsHello(), &WindowsHello::availableChanged, m_secUi->quickUnlockCheckBox, &QCheckBox::setVisible);
#endif
if (hideTouchID) {
m_secUi->touchIDResetCheckBox->setVisible(false);
m_secUi->touchIDResetSpinBox->setVisible(false);
m_secUi->touchIDResetOnScreenLockCheckBox->setVisible(false);
}
m_secUi->quickUnlockCheckBox->setVisible(showQuickUnlock);
}
ApplicationSettingsWidget::~ApplicationSettingsWidget()
@ -313,10 +312,7 @@ void ApplicationSettingsWidget::loadSettings()
m_secUi->EnableCopyOnDoubleClickCheckBox->setChecked(
config()->get(Config::Security_EnableCopyOnDoubleClick).toBool());
m_secUi->touchIDResetCheckBox->setChecked(config()->get(Config::Security_ResetTouchId).toBool());
m_secUi->touchIDResetSpinBox->setValue(config()->get(Config::Security_ResetTouchIdTimeout).toInt());
m_secUi->touchIDResetOnScreenLockCheckBox->setChecked(
config()->get(Config::Security_ResetTouchIdScreenlock).toBool());
m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings();
@ -425,9 +421,7 @@ void ApplicationSettingsWidget::saveSettings()
m_secUi->NoConfirmMoveEntryToRecycleBinCheckBox->isChecked());
config()->set(Config::Security_EnableCopyOnDoubleClick, m_secUi->EnableCopyOnDoubleClickCheckBox->isChecked());
config()->set(Config::Security_ResetTouchId, m_secUi->touchIDResetCheckBox->isChecked());
config()->set(Config::Security_ResetTouchIdTimeout, m_secUi->touchIDResetSpinBox->value());
config()->set(Config::Security_ResetTouchIdScreenlock, m_secUi->touchIDResetOnScreenLockCheckBox->isChecked());
config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked());
// Security: clear storage if related settings are disabled
if (!config()->get(Config::RememberLastDatabases).toBool()) {

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>567</height>
<width>364</width>
<height>493</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
@ -28,93 +28,7 @@
<property name="title">
<string>Timeouts</string>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0,1">
<item row="0" column="1">
<widget class="QSpinBox" name="clearClipboardSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Clipboard clear seconds</string>
</property>
<property name="suffix">
<string comment="Seconds"> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="touchIDResetCheckBox">
<property name="text">
<string>Forget TouchID after inactivity of</string>
</property>
</widget>
</item>
<item row="0" column="2">
<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 row="3" column="1">
<widget class="QSpinBox" name="touchIDResetSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Touch ID inactivity reset</string>
</property>
<property name="suffix">
<string> min</string>
</property>
<property name="maximum">
<number>1440</number>
</property>
<property name="value">
<number>30</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="lockDatabaseIdleCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Lock databases after inactivity of</string>
</property>
</widget>
</item>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0,0">
<item row="1" column="1">
<widget class="QSpinBox" name="lockDatabaseIdleSpinBox">
<property name="enabled">
@ -143,6 +57,20 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="clearClipboardCheckBox">
<property name="text">
<string>Clear clipboard after</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="clearSearchCheckBox">
<property name="text">
<string>Clear search query after</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="clearSearchSpinBox">
<property name="enabled">
@ -171,17 +99,57 @@
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="clearClipboardCheckBox">
<property name="text">
<string>Clear clipboard after</string>
<item row="0" column="1">
<widget class="QSpinBox" name="clearClipboardSpinBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="accessibleName">
<string>Clipboard clear seconds</string>
</property>
<property name="suffix">
<string comment="Seconds"> sec</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="clearSearchCheckBox">
<item row="0" column="2">
<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 row="1" column="0">
<widget class="QCheckBox" name="lockDatabaseIdleCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Clear search query after</string>
<string>Lock databases after inactivity of</string>
</property>
</widget>
</item>
@ -195,16 +163,16 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QCheckBox" name="lockDatabaseOnScreenLockCheckBox">
<widget class="QCheckBox" name="quickUnlockCheckBox">
<property name="text">
<string>Lock databases when session is locked or lid is closed</string>
<string>Enable database quick unlock (Touch ID / Windows Hello)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="touchIDResetOnScreenLockCheckBox">
<widget class="QCheckBox" name="lockDatabaseOnScreenLockCheckBox">
<property name="text">
<string>Forget TouchID when session is locked or lid is closed</string>
<string>Lock databases when session is locked or lid is closed</string>
</property>
</widget>
</item>
@ -308,10 +276,7 @@
<tabstop>lockDatabaseIdleSpinBox</tabstop>
<tabstop>clearSearchCheckBox</tabstop>
<tabstop>clearSearchSpinBox</tabstop>
<tabstop>touchIDResetCheckBox</tabstop>
<tabstop>touchIDResetSpinBox</tabstop>
<tabstop>lockDatabaseOnScreenLockCheckBox</tabstop>
<tabstop>touchIDResetOnScreenLockCheckBox</tabstop>
<tabstop>lockDatabaseMinimizeCheckBox</tabstop>
<tabstop>passwordsRepeatVisibleCheckBox</tabstop>
<tabstop>passwordsHiddenCheckBox</tabstop>

View File

@ -35,7 +35,7 @@ DatabaseOpenDialog::DatabaseOpenDialog(QWidget* parent)
, m_tabBar(new QTabBar(this))
{
setWindowTitle(tr("Unlock Database - KeePassXC"));
setWindowFlags(Qt::Dialog | Qt::WindowStaysOnTopHint);
setWindowFlags(Qt::Dialog);
// block input to the main window/application while the dialog is open
setWindowModality(Qt::ApplicationModal);
#ifdef Q_OS_WIN

View File

@ -30,14 +30,43 @@
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
#ifdef Q_CC_MSVC
#include "winhello/WindowsHello.h"
#endif
#include <QCheckBox>
#include <QDesktopServices>
#include <QFont>
namespace
{
constexpr int clearFormsDelay = 30000;
bool isQuickUnlockAvailable()
{
if (config()->get(Config::Security_QuickUnlock).toBool()) {
#if defined(Q_CC_MSVC)
return getWindowsHello()->isAvailable();
#elif defined(Q_OS_MACOS)
return TouchID::getInstance().isAvailable();
#endif
}
return false;
}
bool canPerformQuickUnlock(const QString& filename)
{
if (isQuickUnlockAvailable()) {
#if defined(Q_CC_MSVC)
return getWindowsHello()->hasKey(filename);
#elif defined(Q_OS_MACOS)
return TouchID::getInstance().containsKey(filename);
#endif
}
Q_UNUSED(filename);
return false;
}
} // namespace
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
: DialogyWidget(parent)
@ -62,8 +91,16 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->labelHeadline->setFont(font);
m_ui->labelHeadline->setText(tr("Unlock KeePassXC Database"));
m_ui->quickUnlockButton->setFont(font);
m_ui->quickUnlockButton->setIcon(
icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText)));
m_ui->quickUnlockButton->setIconSize({32, 32});
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok);
okBtn->setText(tr("Unlock"));
okBtn->setDefault(true);
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
@ -98,13 +135,9 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
m_ui->hardwareKeyProgress->setVisible(false);
#endif
#ifndef WITH_XC_TOUCHID
m_ui->touchIDContainer->setVisible(false);
#else
if (!TouchID::getInstance().isAvailable()) {
m_ui->checkTouchID->setVisible(false);
}
#endif
// QuickUnlock actions
connect(m_ui->quickUnlockButton, &QPushButton::pressed, this, [this] { openDatabase(); });
connect(m_ui->resetQuickUnlockButton, &QPushButton::pressed, this, [this] { resetQuickUnlock(); });
}
DatabaseOpenWidget::~DatabaseOpenWidget()
@ -114,7 +147,14 @@ DatabaseOpenWidget::~DatabaseOpenWidget()
void DatabaseOpenWidget::showEvent(QShowEvent* event)
{
DialogyWidget::showEvent(event);
if (isOnQuickUnlockScreen()) {
m_ui->quickUnlockButton->setFocus();
if (!canPerformQuickUnlock(m_filename)) {
resetQuickUnlock();
}
} else {
m_ui->editPassword->setFocus();
}
m_hideTimer.stop();
}
@ -142,8 +182,12 @@ void DatabaseOpenWidget::load(const QString& filename)
}
}
QHash<QString, QVariant> useTouchID = config()->get(Config::UseTouchID).toHash();
m_ui->checkTouchID->setChecked(useTouchID.value(m_filename, false).toBool());
if (canPerformQuickUnlock(m_filename)) {
m_ui->centralStack->setCurrentIndex(1);
m_ui->quickUnlockButton->setFocus();
} else {
m_ui->editPassword->setFocus();
}
#ifdef WITH_XC_YUBIKEY
// Only auto-poll for hardware keys if we previously used one with this database file
@ -158,12 +202,13 @@ void DatabaseOpenWidget::load(const QString& filename)
void DatabaseOpenWidget::clearForms()
{
setUserInteractionLock(false);
m_ui->editPassword->setText("");
m_ui->editPassword->setShowPassword(false);
m_ui->keyFileLineEdit->clear();
m_ui->keyFileLineEdit->setShowPassword(false);
m_ui->checkTouchID->setChecked(false);
m_ui->challengeResponseCombo->clear();
m_ui->centralStack->setCurrentIndex(0);
m_db.reset();
}
@ -181,32 +226,33 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
{
m_ui->editPassword->setText(pw);
m_ui->keyFileLineEdit->setText(keyFile);
m_blockQuickUnlock = true;
openDatabase();
}
void DatabaseOpenWidget::openDatabase()
{
m_ui->messageWidget->hide();
// Cache this variable for future use then reset
bool blockQuickUnlock = m_blockQuickUnlock || isOnQuickUnlockScreen();
m_blockQuickUnlock = false;
QSharedPointer<CompositeKey> databaseKey = buildDatabaseKey();
setUserInteractionLock(true);
m_ui->messageWidget->hide();
QCoreApplication::processEvents();
const auto databaseKey = buildDatabaseKey();
if (!databaseKey) {
setUserInteractionLock(false);
return;
}
m_ui->editPassword->setShowPassword(false);
QCoreApplication::processEvents();
m_db.reset(new Database());
QString error;
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
m_ui->passwordFormFrame->setEnabled(false);
QCoreApplication::processEvents();
m_db.reset(new Database());
bool ok = m_db->open(m_filename, databaseKey, &error);
QApplication::restoreOverrideCursor();
m_ui->passwordFormFrame->setEnabled(true);
if (ok && m_db->hasMinorVersionMismatch()) {
if (ok) {
// Warn user about minor version mismatch to halt loading if necessary
if (m_db->hasMinorVersionMismatch()) {
QScopedPointer<QMessageBox> msgBox(new QMessageBox(this));
msgBox->setIcon(QMessageBox::Warning);
msgBox->setWindowTitle(tr("Database Version Mismatch"));
@ -222,32 +268,28 @@ void DatabaseOpenWidget::openDatabase()
if (msgBox->clickedButton() != btn) {
m_db.reset(new Database());
m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error);
setUserInteractionLock(false);
return;
}
}
if (ok) {
#ifdef WITH_XC_TOUCHID
QHash<QString, QVariant> useTouchID = config()->get(Config::UseTouchID).toHash();
// check if TouchID can & should be used to unlock the database next time
if (m_ui->checkTouchID->isChecked() && TouchID::getInstance().isAvailable()) {
// encrypt and store key blob
if (TouchID::getInstance().storeKey(m_filename, PasswordKey(m_ui->editPassword->text()).rawKey())) {
useTouchID.insert(m_filename, true);
}
} else {
// when TouchID not available or unchecked, reset for the current database
TouchID::getInstance().reset(m_filename);
useTouchID.insert(m_filename, false);
}
config()->set(Config::UseTouchID, useTouchID);
// Save Quick Unlock credentials if available
if (!blockQuickUnlock && isQuickUnlockAvailable()) {
auto keyData = databaseKey->serialize();
#if defined(Q_CC_MSVC)
// Store the password using Windows Hello
getWindowsHello()->storeKey(m_filename, keyData);
#elif defined(Q_OS_MACOS)
// Store the password using TouchID
TouchID::getInstance().storeKey(m_filename, keyData);
#endif
m_ui->messageWidget->hideMessage();
}
emit dialogFinished(true);
clearForms();
} else {
if (m_ui->editPassword->text().isEmpty() && !m_retryUnlockWithEmptyPassword) {
if (!isOnQuickUnlockScreen() && m_ui->editPassword->text().isEmpty() && !m_retryUnlockWithEmptyPassword) {
QScopedPointer<QMessageBox> msgBox(new QMessageBox(this));
msgBox->setIcon(QMessageBox::Critical);
msgBox->setWindowTitle(tr("Unlock failed and no password given"));
@ -262,21 +304,24 @@ void DatabaseOpenWidget::openDatabase()
if (msgBox->clickedButton() == btn) {
m_retryUnlockWithEmptyPassword = true;
setUserInteractionLock(false);
openDatabase();
return;
}
}
setUserInteractionLock(false);
// Reset quick unlock for the current database
if (isOnQuickUnlockScreen()) {
resetQuickUnlock();
}
m_retryUnlockWithEmptyPassword = false;
m_ui->messageWidget->showMessage(error, MessageWidget::MessageType::Error);
// Focus on the password field and select the input for easy retry
m_ui->editPassword->selectAll();
m_ui->editPassword->setFocus();
#ifdef WITH_XC_TOUCHID
// unable to unlock database, reset TouchID for the current database
TouchID::getInstance().reset(m_filename);
#endif
}
}
@ -284,30 +329,30 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
{
auto databaseKey = QSharedPointer<CompositeKey>::create();
if (canPerformQuickUnlock(m_filename)) {
// try to retrieve the stored password using Windows Hello
QByteArray keyData;
#ifdef Q_CC_MSVC
if (!getWindowsHello()->getKey(m_filename, keyData)) {
// Failed to retrieve Quick Unlock data
m_ui->messageWidget->showMessage(tr("Failed to authenticate with Windows Hello"), MessageWidget::Error);
return {};
}
#elif defined(Q_OS_MACOS)
if (!TouchID::getInstance().getKey(m_filename, keyData)) {
// Failed to retrieve Quick Unlock data
m_ui->messageWidget->showMessage(tr("Failed to authenticate with Touch ID"), MessageWidget::Error);
return {};
}
#endif
databaseKey->setRawKey(keyData);
return databaseKey;
}
if (!m_ui->editPassword->text().isEmpty() || m_retryUnlockWithEmptyPassword) {
databaseKey->addKey(QSharedPointer<PasswordKey>::create(m_ui->editPassword->text()));
}
#ifdef WITH_XC_TOUCHID
// check if TouchID is available and enabled for unlocking the database
if (m_ui->checkTouchID->isChecked() && TouchID::getInstance().isAvailable()
&& m_ui->editPassword->text().isEmpty()) {
// clear empty password from composite key
databaseKey->clear();
// try to get, decrypt and use PasswordKey
QByteArray passwordKey;
if (TouchID::getInstance().getKey(m_filename, passwordKey)) {
// check if the user cancelled the operation
if (passwordKey.isNull()) {
return QSharedPointer<CompositeKey>();
}
databaseKey->addKey(PasswordKey::fromRawKey(passwordKey));
}
}
#endif
auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash();
lastKeyFiles.remove(m_filename);
@ -465,3 +510,32 @@ void DatabaseOpenWidget::openKeyFileHelp()
{
QDesktopServices::openUrl(QUrl("https://keepassxc.org/docs#faq-cat-keyfile"));
}
void DatabaseOpenWidget::setUserInteractionLock(bool state)
{
if (state) {
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
m_ui->centralStack->setEnabled(false);
} else {
// Ensure no override cursors remain
while (QApplication::overrideCursor()) {
QApplication::restoreOverrideCursor();
}
m_ui->centralStack->setEnabled(true);
}
}
bool DatabaseOpenWidget::isOnQuickUnlockScreen()
{
return m_ui->centralStack->currentIndex() == 1;
}
void DatabaseOpenWidget::resetQuickUnlock()
{
#if defined(Q_CC_MSVC)
getWindowsHello()->reset(m_filename);
#elif defined(Q_OS_MACOS)
TouchID::getInstance().reset(m_filename);
#endif
load(m_filename);
}

View File

@ -53,6 +53,10 @@ protected:
void showEvent(QShowEvent* event) override;
void hideEvent(QHideEvent* event) override;
QSharedPointer<CompositeKey> buildDatabaseKey();
void setUserInteractionLock(bool state);
// Quick Unlock helper functions
bool isOnQuickUnlockScreen();
void resetQuickUnlock();
const QScopedPointer<Ui::DatabaseOpenWidget> m_ui;
QSharedPointer<Database> m_db;
@ -73,6 +77,7 @@ private slots:
private:
bool m_pollingHardwareKey = false;
bool m_blockQuickUnlock = false;
QTimer m_hideTimer;
Q_DISABLE_COPY(DatabaseOpenWidget)

View File

@ -6,41 +6,22 @@
<rect>
<x>0</x>
<y>0</y>
<width>588</width>
<height>448</height>
<width>520</width>
<height>436</height>
</rect>
</property>
<property name="accessibleName">
<string>Unlock KeePassXC Database</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0,2">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0">
<item>
<widget class="MessageWidget" name="messageWidget" native="true"/>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="0,1,0">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
@ -59,16 +40,32 @@
</item>
<item>
<widget class="QWidget" name="formContainer" native="true">
<property name="minimumSize">
<size>
<width>500</width>
<height>400</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>700</width>
<height>16777215</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="1,0,0,0,0,2">
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="labelHeadline">
<property name="font">
@ -107,26 +104,24 @@
</spacer>
</item>
<item>
<widget class="QFrame" name="loginFrame">
<widget class="QStackedWidget" name="centralStack">
<property name="minimumSize">
<size>
<width>550</width>
<height>0</height>
<width>0</width>
<height>250</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>2</number>
<number>1</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="mainPage">
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="leftMargin">
<number>20</number>
</property>
@ -139,21 +134,6 @@
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<widget class="QFrame" name="passwordFormFrame">
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>700</width>
<height>16777215</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
@ -199,9 +179,6 @@
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
@ -220,9 +197,6 @@
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="topMargin">
<number>3</number>
</property>
@ -262,7 +236,7 @@
}</string>
</property>
<property name="text">
<string>?</string>
<string notr="true">?</string>
</property>
<property name="iconSize">
<size>
@ -482,47 +456,6 @@
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="touchIDContainer" native="true">
<layout class="QHBoxLayout" name="touchIDLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="checkTouchID">
<property name="text">
<string>TouchID for Quick Unlock</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="dialogButtonsLayout">
<property name="topMargin">
@ -531,7 +464,7 @@
<item alignment="Qt::AlignRight">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
<set>QDialogButtonBox::Close|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
@ -539,9 +472,125 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="quickUnlockPage">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>20</number>
</property>
<property name="topMargin">
<number>15</number>
</property>
<property name="rightMargin">
<number>20</number>
</property>
<property name="bottomMargin">
<number>15</number>
</property>
<item>
<spacer name="horizontalSpacer_5">
<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>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="quickUnlockButton">
<property name="minimumSize">
<size>
<width>200</width>
<height>60</height>
</size>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Unlock Database</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetQuickUnlockButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_8">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_6">
<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>
</widget>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>55</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
@ -564,22 +613,6 @@
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::MinimumExpanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>55</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
@ -607,7 +640,8 @@
<tabstop>buttonBrowseFile</tabstop>
<tabstop>challengeResponseCombo</tabstop>
<tabstop>buttonRedetectYubikey</tabstop>
<tabstop>checkTouchID</tabstop>
<tabstop>quickUnlockButton</tabstop>
<tabstop>resetQuickUnlockButton</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -36,13 +36,14 @@
class AdaptiveIconEngine : public QIconEngine
{
public:
explicit AdaptiveIconEngine(QIcon baseIcon);
explicit AdaptiveIconEngine(QIcon baseIcon, QColor overrideColor = {});
void paint(QPainter* painter, const QRect& rect, QIcon::Mode mode, QIcon::State state) override;
QPixmap pixmap(const QSize& size, QIcon::Mode mode, QIcon::State state) override;
QIconEngine* clone() const override;
private:
QIcon m_baseIcon;
QColor m_overrideColor;
};
Icons* Icons::m_instance(nullptr);
@ -113,9 +114,10 @@ QIcon Icons::trayIconUnlocked()
return trayIcon("unlocked");
}
AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon)
AdaptiveIconEngine::AdaptiveIconEngine(QIcon baseIcon, QColor overrideColor)
: QIconEngine()
, m_baseIcon(std::move(baseIcon))
, m_overrideColor(overrideColor)
{
}
@ -133,7 +135,10 @@ void AdaptiveIconEngine::paint(QPainter* painter, const QRect& rect, QIcon::Mode
m_baseIcon.paint(&p, img.rect(), Qt::AlignCenter, mode, state);
if (getMainWindow()) {
if (m_overrideColor.isValid()) {
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
p.fillRect(img.rect(), m_overrideColor);
} else if (getMainWindow()) {
QPalette palette = getMainWindow()->palette();
p.setCompositionMode(QPainter::CompositionMode_SourceIn);
@ -188,7 +193,7 @@ QIcon Icons::icon(const QString& name, bool recolor, const QColor& overrideColor
icon = QIcon::fromTheme(name);
if (recolor) {
icon = QIcon(new AdaptiveIconEngine(icon));
icon = QIcon(new AdaptiveIconEngine(icon, overrideColor));
#if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)
icon.setIsMask(true);
#endif

View File

@ -44,12 +44,6 @@
#include "gui/SearchWidget.h"
#include "gui/osutils/OSUtils.h"
#ifdef Q_OS_MACOS
#ifdef WITH_XC_TOUCHID
#include "touchid/TouchID.h"
#endif
#endif
#ifdef WITH_XC_UPDATECHECK
#include "gui/UpdateCheckDialog.h"
#include "updatecheck/UpdateChecker.h"
@ -259,10 +253,6 @@ MainWindow::MainWindow()
m_inactivityTimer = new InactivityTimer(this);
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity()));
#ifdef WITH_XC_TOUCHID
m_touchIDinactivityTimer = new InactivityTimer(this);
connect(m_touchIDinactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(forgetTouchIDAfterInactivity()));
#endif
applySettingsChanges();
m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N);
@ -1537,21 +1527,6 @@ void MainWindow::applySettingsChanges()
m_inactivityTimer->deactivate();
}
#ifdef WITH_XC_TOUCHID
if (config()->get(Config::Security_ResetTouchId).toBool()) {
// Calculate TouchID timeout in milliseconds
timeout = config()->get(Config::Security_ResetTouchIdTimeout).toInt() * 60 * 1000;
if (timeout <= 0) {
timeout = 30 * 60 * 1000;
}
m_touchIDinactivityTimer->setInactivityTimeout(timeout);
m_touchIDinactivityTimer->activate();
} else {
m_touchIDinactivityTimer->deactivate();
}
#endif
m_ui->toolBar->setHidden(config()->get(Config::GUI_HideToolbar).toBool());
m_ui->toolBar->setMovable(config()->get(Config::GUI_MovableToolbar).toBool());
@ -1715,13 +1690,6 @@ void MainWindow::lockDatabasesAfterInactivity()
m_ui->tabWidget->lockDatabases();
}
void MainWindow::forgetTouchIDAfterInactivity()
{
#ifdef WITH_XC_TOUCHID
TouchID::getInstance().reset();
#endif
}
bool MainWindow::isTrayIconEnabled() const
{
return m_trayIcon && m_trayIcon->isVisible();
@ -1778,12 +1746,6 @@ void MainWindow::handleScreenLock()
if (config()->get(Config::Security_LockDatabaseScreenLock).toBool()) {
lockDatabasesAfterInactivity();
}
#ifdef WITH_XC_TOUCHID
if (config()->get(Config::Security_ResetTouchIdScreenlock).toBool()) {
forgetTouchIDAfterInactivity();
}
#endif
}
QStringList MainWindow::kdbxFilesFromUrls(const QList<QUrl>& urls)

View File

@ -134,7 +134,6 @@ private slots:
void trayIconTriggered(QSystemTrayIcon::ActivationReason reason);
void processTrayIconTrigger();
void lockDatabasesAfterInactivity();
void forgetTouchIDAfterInactivity();
void handleScreenLock();
void showErrorMessage(const QString& message);
void selectNextDatabaseTab();

View File

@ -16,7 +16,7 @@
<property name="minimumSize">
<size>
<width>800</width>
<height>400</height>
<height>500</height>
</size>
</property>
<property name="windowTitle">
@ -1091,18 +1091,18 @@
</action>
</widget>
<customwidgets>
<customwidget>
<class>PasswordGeneratorWidget</class>
<extends>QWidget</extends>
<header>gui/PasswordGeneratorWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>MessageWidget</class>
<extends>QWidget</extends>
<header>gui/MessageWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PasswordGeneratorWidget</class>
<extends>QWidget</extends>
<header>gui/PasswordGeneratorWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>DatabaseTabWidget</class>
<extends>QTabWidget</extends>

View File

@ -32,9 +32,6 @@
#ifdef WITH_XC_FDOSECRETS
#include "fdosecrets/DatabaseSettingsPageFdoSecrets.h"
#endif
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
#include "core/Config.h"
#include "core/Database.h"
@ -184,10 +181,6 @@ void DatabaseSettingsDialog::save()
extraPage.saveSettings();
}
#ifdef WITH_XC_TOUCHID
TouchID::getInstance().reset(m_db ? m_db->filePath() : "");
#endif
emit editFinished(true);
}

View File

@ -26,6 +26,13 @@
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
#ifdef Q_CC_MSVC
#include "winhello/WindowsHello.h"
#endif
#include <QLayout>
#include <QPushButton>
@ -193,6 +200,12 @@ bool DatabaseSettingsWidgetDatabaseKey::save()
m_db->setKey(newKey, true, false, false);
#if defined(Q_OS_MACOS)
TouchID::getInstance().reset(m_db->filePath());
#elif defined(Q_CC_MSVC)
getWindowsHello()->reset(m_db->filePath());
#endif
emit editFinished(true);
if (m_isDirty) {
m_db->markAsModified();

View File

@ -121,14 +121,6 @@ void ReportsDialog::addPage(QSharedPointer<IReportsPage> page)
void ReportsDialog::reject()
{
for (const ExtraPage& extraPage : asConst(m_extraPages)) {
extraPage.saveSettings();
}
#ifdef WITH_XC_TOUCHID
TouchID::getInstance().reset(m_db ? m_db->filePath() : "");
#endif
emit editFinished(true);
}

View File

@ -37,8 +37,8 @@ EntryPreviewWidget TagsEdit
border: none;
}
DatabaseOpenWidget #loginFrame {
border: 2px groove palette(mid);
DatabaseOpenWidget #centralStack {
border: 1px solid palette(mid);
background: palette(light);
}

View File

@ -1,4 +1,4 @@
DatabaseOpenWidget #loginFrame {
DatabaseOpenWidget #centralStack {
border: 2px groove palette(mid);
background: palette(light);
}

View File

@ -209,7 +209,6 @@ QByteArray CompositeKey::serialize() const
for (auto const& key : m_challengeResponseKeys) {
stream << key->uuid().toRfc4122() << key->serialize();
}
return data;
}

View File

@ -32,6 +32,8 @@ public:
bool getKey(const QString& databasePath, QByteArray& passwordKey) const;
bool containsKey(const QString& databasePath) const;
bool isAvailable();
bool authenticate(const QString& message = "") const;

View File

@ -57,11 +57,11 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
}
// generate random AES 256bit key and IV
QByteArray randomKey = randomGen()->randomArray(32);
QByteArray randomIV = randomGen()->randomArray(16);
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_CBC, SymmetricCipher::Encrypt, randomKey, randomIV)) {
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
debug("TouchID::storeKey - Error initializing encryption: %s",
aes256Encrypt.errorString().toUtf8().constData());
return false;
@ -69,8 +69,9 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
// encrypt and keep result in memory
QByteArray encryptedMasterKey = passwordKey;
if (!aes256Encrypt.process(encryptedMasterKey)) {
if (!aes256Encrypt.finish(encryptedMasterKey)) {
debug("TouchID::storeKey - Error encrypting: %s", aes256Encrypt.errorString().toUtf8().constData());
debug(aes256Encrypt.errorString().toUtf8().constData());
return false;
}
@ -166,7 +167,7 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
}
// checks if encrypted PasswordKey is available and is stored for the given database
if (!this->m_encryptedMasterKeys.contains(databasePath)) {
if (!containsKey(databasePath)) {
debug("TouchID::getKey - No stored key found");
return false;
}
@ -205,18 +206,19 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
CFRelease(valueData);
// extract AES key and IV from data bytes
QByteArray key = dataBytes.left(32);
QByteArray iv = dataBytes.right(16);
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_CBC, SymmetricCipher::Decrypt, key, iv)) {
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
debug("TouchID::getKey - Error initializing decryption: %s", aes256Decrypt.errorString().toUtf8().constData());
return false;
}
// decrypt PasswordKey from memory using AES
passwordKey = m_encryptedMasterKeys[databasePath];
if (!aes256Decrypt.process(passwordKey)) {
if (!aes256Decrypt.finish(passwordKey)) {
passwordKey.clear();
debug("TouchID::getKey - Error decryption: %s", aes256Decrypt.errorString().toUtf8().constData());
return false;
}
@ -224,6 +226,11 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
return true;
}
bool TouchID::containsKey(const QString& dbPath) const
{
return m_encryptedMasterKeys.contains(dbPath);
}
/**
* Dynamic check if TouchID is available on the current machine.
*/

View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2022 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 "WindowsHello.h"
#include <Userconsentverifierinterop.h>
#include <winrt/base.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.security.credentials.h>
#include <winrt/windows.security.cryptography.h>
#include <winrt/windows.storage.streams.h>
#include "core/AsyncTask.h"
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include <QTimer>
#include <QWindow>
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Security::Credentials;
using namespace Windows::Security::Cryptography;
using namespace Windows::Storage::Streams;
namespace
{
const std::wstring s_winHelloKeyName{L"keepassxc_winhello"};
void queueSecurityPromptFocus(int delay = 500)
{
QTimer::singleShot(delay, [] {
auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr);
if (hWnd) {
::SetForegroundWindow(hWnd);
}
});
}
bool deriveEncryptionKey(QByteArray& challenge, QByteArray& key, QString& error)
{
error.clear();
auto challengeBuffer = CryptographicBuffer::CreateFromByteArray(
array_view<uint8_t>(reinterpret_cast<uint8_t*>(challenge.data()), challenge.size()));
return AsyncTask::runAndWaitForFuture([&] {
// The first time this is used a key-pair will be generated using the common name
auto result =
KeyCredentialManager::RequestCreateAsync(s_winHelloKeyName, KeyCredentialCreationOption::FailIfExists)
.get();
if (result.Status() == KeyCredentialStatus::CredentialAlreadyExists) {
result = KeyCredentialManager::OpenAsync(s_winHelloKeyName).get();
} else if (result.Status() != KeyCredentialStatus::Success) {
error = QObject::tr("Failed to create Windows Hello credential.");
return false;
}
const auto signature = result.Credential().RequestSignAsync(challengeBuffer).get();
if (signature.Status() != KeyCredentialStatus::Success) {
error = QObject::tr("Failed to sign challenge using Windows Hello.");
return false;
}
// Use the SHA-256 hash of the challenge signature as the encryption key
const auto response = signature.Result();
CryptoHash hasher(CryptoHash::Sha256);
hasher.addData({reinterpret_cast<const char*>(response.data()), static_cast<int>(response.Length())});
key = hasher.result();
return true;
});
}
} // namespace
WindowsHello* WindowsHello::m_instance{nullptr};
WindowsHello* WindowsHello::instance()
{
if (!m_instance) {
m_instance = new WindowsHello();
}
return m_instance;
}
WindowsHello::WindowsHello(QObject* parent)
: QObject(parent)
{
concurrency::create_task([this] {
bool state = KeyCredentialManager::IsSupportedAsync().get();
m_available = state;
emit availableChanged(m_available);
});
}
bool WindowsHello::isAvailable() const
{
return m_available;
}
QString WindowsHello::errorString() const
{
return m_error;
}
bool WindowsHello::storeKey(const QString& dbPath, const QByteArray& data)
{
queueSecurityPromptFocus();
// Generate a random challenge that will be signed by Windows Hello
// to create the key. The challenge is also used as the IV.
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
auto challenge = Random::instance()->randomArray(ivSize);
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
return false;
}
// Encrypt the data using AES-256-CBC
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, challenge)) {
m_error = tr("Failed to init KeePassXC crypto.");
return false;
}
QByteArray encrypted = data;
if (!cipher.finish(encrypted)) {
m_error = tr("Failed to encrypt key data.");
return false;
}
// Prepend the challenge/IV to the encrypted data
encrypted.prepend(challenge);
m_encryptedKeys.insert(dbPath, encrypted);
return true;
}
bool WindowsHello::getKey(const QString& dbPath, QByteArray& data)
{
data.clear();
if (!hasKey(dbPath)) {
m_error = tr("Failed to get Windows Hello credential.");
return false;
}
queueSecurityPromptFocus();
// Read the previously used challenge and encrypted data
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& keydata = m_encryptedKeys.value(dbPath);
auto challenge = keydata.left(ivSize);
auto encrypted = keydata.mid(ivSize);
QByteArray key;
if (!deriveEncryptionKey(challenge, key, m_error)) {
return false;
}
// Decrypt the data using the generated key and IV from above
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) {
m_error = tr("Failed to init KeePassXC crypto.");
return false;
}
// Store the decrypted data into the passed parameter
data = encrypted;
if (!cipher.finish(data)) {
data.clear();
m_error = tr("Failed to decrypt key data.");
return false;
}
return true;
}
void WindowsHello::reset(const QString& dbPath)
{
m_encryptedKeys.remove(dbPath);
}
bool WindowsHello::hasKey(const QString& dbPath) const
{
return m_encryptedKeys.contains(dbPath);
}
void WindowsHello::reset()
{
m_encryptedKeys.clear();
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2022 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_WINDOWSHELLO_H
#define KEEPASSXC_WINDOWSHELLO_H
#include <QHash>
#include <QObject>
class WindowsHello : public QObject
{
Q_OBJECT
public:
static WindowsHello* instance();
bool isAvailable() const;
QString errorString() const;
void reset();
bool storeKey(const QString& dbPath, const QByteArray& key);
bool getKey(const QString& dbPath, QByteArray& key);
bool hasKey(const QString& dbPath) const;
void reset(const QString& dbPath);
signals:
void availableChanged(bool state);
private:
bool m_available = false;
QString m_error;
QHash<QString, QByteArray> m_encryptedKeys;
static WindowsHello* m_instance;
WindowsHello(QObject* parent = nullptr);
~WindowsHello() override = default;
Q_DISABLE_COPY(WindowsHello);
};
inline WindowsHello* getWindowsHello()
{
return WindowsHello::instance();
}
#endif // KEEPASSXC_WINDOWSHELLO_H