From 4f0710350fb53c25db6824f96961d8d17a29347c Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Mon, 21 Feb 2022 20:40:01 -0500 Subject: [PATCH] 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. --- CMakeLists.txt | 6 - COPYING | 3 +- INSTALL.md | 1 - .../scalable/actions/fingerprint.svg | 1 + share/icons/icons.qrc | 2 +- share/translations/keepassxc_en.ts | 83 +- src/CMakeLists.txt | 24 +- src/config-keepassx.h.cmake | 1 - src/core/Config.cpp | 14 +- src/core/Config.h | 5 +- src/core/Tools.cpp | 4 +- src/crypto/SymmetricCipher.cpp | 7 + src/crypto/SymmetricCipher.h | 1 + src/gui/ApplicationSettingsWidget.cpp | 30 +- src/gui/ApplicationSettingsWidgetSecurity.ui | 171 ++-- src/gui/DatabaseOpenDialog.cpp | 2 +- src/gui/DatabaseOpenWidget.cpp | 240 ++++-- src/gui/DatabaseOpenWidget.h | 5 + src/gui/DatabaseOpenWidget.ui | 804 +++++++++--------- src/gui/Icons.cpp | 13 +- src/gui/MainWindow.cpp | 38 - src/gui/MainWindow.h | 1 - src/gui/MainWindow.ui | 14 +- src/gui/dbsettings/DatabaseSettingsDialog.cpp | 7 - .../DatabaseSettingsWidgetDatabaseKey.cpp | 13 + src/gui/reports/ReportsDialog.cpp | 8 - src/gui/styles/base/basestyle.qss | 4 +- src/gui/styles/base/classicstyle.qss | 2 +- src/keys/CompositeKey.cpp | 1 - src/touchid/TouchID.h | 2 + src/touchid/TouchID.mm | 25 +- src/winhello/WindowsHello.cpp | 202 +++++ src/winhello/WindowsHello.h | 58 ++ 33 files changed, 1058 insertions(+), 734 deletions(-) create mode 100644 share/icons/application/scalable/actions/fingerprint.svg create mode 100644 src/winhello/WindowsHello.cpp create mode 100644 src/winhello/WindowsHello.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 216a03a17..def89ea89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/COPYING b/COPYING index 65b7554d8..17dfe4755 100644 --- a/COPYING +++ b/COPYING @@ -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 +Copyright: 2022 KeePassXC Team 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 diff --git a/INSTALL.md b/INSTALL.md index b25dd38b8..0f5f3ae86 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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) diff --git a/share/icons/application/scalable/actions/fingerprint.svg b/share/icons/application/scalable/actions/fingerprint.svg new file mode 100644 index 000000000..c6e469f73 --- /dev/null +++ b/share/icons/application/scalable/actions/fingerprint.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index 86a3abe39..61cbc9103 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -39,6 +39,7 @@ application/scalable/actions/entry-edit.svg application/scalable/actions/entry-new.svg application/scalable/actions/favicon-download.svg + application/scalable/actions/fingerprint.svg application/scalable/actions/getting-started.svg application/scalable/actions/group-delete.svg application/scalable/actions/group-edit.svg @@ -80,7 +81,6 @@ application/scalable/actions/username-copy.svg application/scalable/actions/view-history.svg application/scalable/actions/web.svg - application/scalable/apps/freedesktop.svg application/scalable/apps/internet-web-browser.svg application/scalable/apps/keepassxc.svg diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 71649a3e8..92d8de75d 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -508,14 +508,6 @@ Lock databases after inactivity of Lock databases after inactivity of - - min - min - - - Forget TouchID after inactivity of - Forget TouchID after inactivity of - Convenience Convenience @@ -524,10 +516,6 @@ Lock databases when session is locked or lid is closed Lock databases when session is locked or lid is closed - - Forget TouchID when session is locked or lid is closed - Forget TouchID when session is locked or lid is closed - Lock databases after minimizing the window Lock databases after minimizing the window @@ -552,10 +540,6 @@ Clipboard clear seconds - - Touch ID inactivity reset - - Database lock timeout seconds @@ -589,6 +573,10 @@ Enable double click to copy the username/password entry columns + + Enable database quick unlock (Touch ID / Windows Hello) + + AutoType @@ -1474,10 +1462,6 @@ Backup database located at %2 Hardware key help - - TouchID for Quick Unlock - - Unlock failed and no password given @@ -1501,10 +1485,6 @@ To prevent this error from appearing, you must go to "Database Settings / S Key file help - - ? - - Cannot use database file as key file @@ -1577,6 +1557,26 @@ We recommend you update your KeePassXC installation. Database unlock canceled. + + Unlock + + + + Failed to authenticate with Windows Hello + + + + Unlock Database + + + + Cancel + Cancel + + + Failed to authenticate with Touch ID + + DatabaseSettingWidgetMetaData @@ -6837,10 +6837,6 @@ Kernel: %3 %4 YubiKey - - TouchID - - None @@ -7770,6 +7766,18 @@ Please consider generating a new key file. Browser Statistics + + Quick Unlock + + + + Failed to create Windows Hello credential. + + + + Failed to sign challenge using Windows Hello. + + QtIOCompressor @@ -8762,6 +8770,25 @@ Example: JBSWY3DPEHPK3PXP + + WindowsHello + + Failed to init KeePassXC crypto. + + + + Failed to encrypt key data. + + + + Failed to get Windows Hello credential. + + + + Failed to decrypt key data. + + + YubiKey diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9e0f2b356..c7291a638 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 - CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements") - endif() + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist + CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements") if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib") install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib" diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake index 53c3f03cb..b80bc48d9 100644 --- a/src/config-keepassx.h.cmake +++ b/src/config-keepassx.h.cmake @@ -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@" diff --git a/src/core/Config.cpp b/src/core/Config.cpp index af8973646..6bdef2f4a 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -81,7 +81,6 @@ static const QHash 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 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 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 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. diff --git a/src/core/Config.h b/src/core/Config.h index 5ab14b5b1..be7a736f8 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -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, diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 5d3dff602..867d8c174 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -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"); diff --git a/src/crypto/SymmetricCipher.cpp b/src/crypto/SymmetricCipher.cpp index b21a1ef47..4d3a7bdfe 100644 --- a/src/crypto/SymmetricCipher.cpp +++ b/src/crypto/SymmetricCipher.cpp @@ -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: diff --git a/src/crypto/SymmetricCipher.h b/src/crypto/SymmetricCipher.h index f666582f7..83b54658f 100644 --- a/src/crypto/SymmetricCipher.h +++ b/src/crypto/SymmetricCipher.h @@ -39,6 +39,7 @@ public: Twofish_CBC, ChaCha20, Salsa20, + Aes256_GCM, InvalidMode = -1, }; diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 7620c9ba5..9dd9e3df8 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -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()) { diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui index bf4b984e1..5ca7d69e0 100644 --- a/src/gui/ApplicationSettingsWidgetSecurity.ui +++ b/src/gui/ApplicationSettingsWidgetSecurity.ui @@ -6,8 +6,8 @@ 0 0 - 595 - 567 + 364 + 493 @@ -28,93 +28,7 @@ Timeouts - - - - - false - - - - 0 - 0 - - - - Clipboard clear seconds - - - sec - - - 1 - - - 999 - - - 10 - - - - - - - Forget TouchID after inactivity of - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - - 0 - 0 - - - - Touch ID inactivity reset - - - min - - - 1440 - - - 30 - - - - - - - - 0 - 0 - - - - Lock databases after inactivity of - - - + @@ -143,6 +57,20 @@ + + + + Clear clipboard after + + + + + + + Clear search query after + + + @@ -171,17 +99,57 @@ - - - - Clear clipboard after + + + + false + + + + 0 + 0 + + + + Clipboard clear seconds + + + sec + + + 1 + + + 999 + + + 10 - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + - Clear search query after + Lock databases after inactivity of @@ -195,16 +163,16 @@ - + - Lock databases when session is locked or lid is closed + Enable database quick unlock (Touch ID / Windows Hello) - + - Forget TouchID when session is locked or lid is closed + Lock databases when session is locked or lid is closed @@ -308,10 +276,7 @@ lockDatabaseIdleSpinBox clearSearchCheckBox clearSearchSpinBox - touchIDResetCheckBox - touchIDResetSpinBox lockDatabaseOnScreenLockCheckBox - touchIDResetOnScreenLockCheckBox lockDatabaseMinimizeCheckBox passwordsRepeatVisibleCheckBox passwordsHiddenCheckBox diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp index cfa0fcadf..1fd4f1c0d 100644 --- a/src/gui/DatabaseOpenDialog.cpp +++ b/src/gui/DatabaseOpenDialog.cpp @@ -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 diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 1d51b5ebe..23719a3c1 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -30,14 +30,43 @@ #ifdef Q_OS_MACOS #include "touchid/TouchID.h" #endif +#ifdef Q_CC_MSVC +#include "winhello/WindowsHello.h" +#endif +#include #include #include 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); - m_ui->editPassword->setFocus(); + 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 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,73 +226,70 @@ 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 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()) { - QScopedPointer msgBox(new QMessageBox(this)); - msgBox->setIcon(QMessageBox::Warning); - msgBox->setWindowTitle(tr("Database Version Mismatch")); - msgBox->setText(tr("The database you are trying to open was most likely\n" - "created by a newer version of KeePassXC.\n\n" - "You can try to open it anyway, but it may be incomplete\n" - "and saving any changes may incur data loss.\n\n" - "We recommend you update your KeePassXC installation.")); - auto btn = msgBox->addButton(tr("Open database anyway"), QMessageBox::ButtonRole::AcceptRole); - msgBox->setDefaultButton(btn); - msgBox->addButton(QMessageBox::Cancel); - msgBox->exec(); - if (msgBox->clickedButton() != btn) { - m_db.reset(new Database()); - m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error); - return; - } - } if (ok) { -#ifdef WITH_XC_TOUCHID - QHash 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); + // Warn user about minor version mismatch to halt loading if necessary + if (m_db->hasMinorVersionMismatch()) { + QScopedPointer msgBox(new QMessageBox(this)); + msgBox->setIcon(QMessageBox::Warning); + msgBox->setWindowTitle(tr("Database Version Mismatch")); + msgBox->setText(tr("The database you are trying to open was most likely\n" + "created by a newer version of KeePassXC.\n\n" + "You can try to open it anyway, but it may be incomplete\n" + "and saving any changes may incur data loss.\n\n" + "We recommend you update your KeePassXC installation.")); + auto btn = msgBox->addButton(tr("Open database anyway"), QMessageBox::ButtonRole::AcceptRole); + msgBox->setDefaultButton(btn); + msgBox->addButton(QMessageBox::Cancel); + msgBox->exec(); + if (msgBox->clickedButton() != btn) { + m_db.reset(new Database()); + m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error); + setUserInteractionLock(false); + return; } - } 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 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 DatabaseOpenWidget::buildDatabaseKey() { auto databaseKey = QSharedPointer::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::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(); - } - - 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); +} diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index 1742aeb2c..df5339bf6 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -53,6 +53,10 @@ protected: void showEvent(QShowEvent* event) override; void hideEvent(QHideEvent* event) override; QSharedPointer buildDatabaseKey(); + void setUserInteractionLock(bool state); + // Quick Unlock helper functions + bool isOnQuickUnlockScreen(); + void resetQuickUnlock(); const QScopedPointer m_ui; QSharedPointer m_db; @@ -73,6 +77,7 @@ private slots: private: bool m_pollingHardwareKey = false; + bool m_blockQuickUnlock = false; QTimer m_hideTimer; Q_DISABLE_COPY(DatabaseOpenWidget) diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 20813008d..101bef632 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -6,41 +6,22 @@ 0 0 - 588 - 448 + 520 + 436 Unlock KeePassXC Database - + - - - - Qt::Vertical - - - QSizePolicy::MinimumExpanding - - - - 20 - 5 - - - - 0 - - QLayout::SetDefaultConstraint - @@ -59,16 +40,32 @@ + + + 500 + 400 + + 700 16777215 - - - QLayout::SetMinimumSize - + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -107,142 +104,223 @@ - + - 550 - 0 + 0 + 250 QFrame::StyledPanel - - QFrame::Plain - - 2 + 1 - - - QLayout::SetMinimumSize - - - 20 - - - 15 - - - 20 - - - 15 - - - - - - 400 - 0 - - - - - 700 - 16777215 - - - + + 0 + + + + + 20 + + + 15 + + + 20 + + + 15 + + + + + Enter Password: + + + editPassword + + + + + + + Password field + + + QLineEdit::Password + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 5 + + + + + + + + Enter Additional Credentials (if any): + + + + + - - - Enter Password: - - - editPassword - - - - - - - Password field - - - QLineEdit::Password - - - - - + - Qt::Vertical + Qt::Horizontal QSizePolicy::Fixed - 20 - 5 + 15 + 20 - - - Enter Additional Credentials (if any): + + + 3 - - - - - - QLayout::SetMinimumSize - - - - - Qt::Horizontal + + + + 5 - - QSizePolicy::Fixed - - - - 15 - 20 - - - + + + + Key File: + + + keyFileLineEdit + + + + + + + PointingHandCursor + + + Qt::ClickFocus + + + <p>In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database's security settings.</p><p>This is <strong>not</strong> your *.kdbx database file!<br>If you do not have a key file, leave this field empty.</p><p>Click for more information…</p> + + + Key file help + + + QToolButton { + border: none; + background: none; +} + + + ? + + + + 12 + 12 + + + + QToolButton::InstantPopup + + + + - - - - QLayout::SetMinimumSize + + + + 0 - - 3 + + + + + 16777215 + 2 + + + + 0 + + + 0 + + + -1 + + + false + + + + + + + false + + + + 0 + 0 + + + + Hardware key slot selection + + + false + + + + + + + + + 2 - - + + 5 - + - Key File: + Hardware Key: - keyFileLineEdit + challengeResponseCombo - + PointingHandCursor @@ -250,10 +328,11 @@ Qt::ClickFocus - <p>In addition to a password, you can use a secret file to enhance the security of your database. This file can be generated in your database's security settings.</p><p>This is <strong>not</strong> your *.kdbx database file!<br>If you do not have a key file, leave this field empty.</p><p>Click for more information…</p> + <p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> +<p>Click for more information…</p> - Key file help + Hardware key help QToolButton { @@ -262,7 +341,7 @@ } - ? + ? @@ -277,272 +356,242 @@ - - - - 0 + + + + Qt::Vertical - - - - - 16777215 - 2 - - - - 0 - - - 0 - - - -1 - - - false - - - - - - - false - - - - 0 - 0 - - - - Hardware key slot selection - - - false - - - - - - - - - 2 + + QSizePolicy::Fixed - - - - 5 - - - - - Hardware Key: - - - challengeResponseCombo - - - - - - - PointingHandCursor - - - Qt::ClickFocus - - - <p>You can use a hardware security key such as a <strong>YubiKey</strong> or <strong>OnlyKey</strong> with slots configured for HMAC-SHA1.</p> -<p>Click for more information…</p> - - - Hardware key help - - - QToolButton { - border: none; - background: none; -} - - - ? - - - - 12 - 12 - - - - QToolButton::InstantPopup - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 2 - - - - - - - - - - 0 + + + 0 + 2 + - - - - - 0 - 0 - - - - Key file to unlock the database - - - QLineEdit::Password - - - true - - - - + - - - - Browse for key file + + + + + + 0 + + + + + + 0 + 0 + - Browse for key file + Key file to unlock the database - - Browse… + + QLineEdit::Password + + + true - - - - 0 + + + + + + Browse for key file + + + Browse for key file + + + Browse… + + + + + + + 0 + + + + + true - - - - true - - - Refresh hardware tokens - - - Refresh hardware tokens - - - Refresh - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 2 - - - - - + + Refresh hardware tokens + + + Refresh hardware tokens + + + Refresh + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 2 + + + + + + + + + 15 + + + + + QDialogButtonBox::Close|QDialogButtonBox::Ok + + + + + + + + + + + 20 + + + 15 + + + 20 + + + 15 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - - 40 - 0 - - - - - - - - TouchID for Quick Unlock - - - - + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 200 + 60 + + + + + 10 + 75 + true + + + + Unlock Database + + + true + - - - 15 + + + Cancel - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + + + + + + Qt::Vertical + + + + 20 + 40 + + + - - - + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 20 + 55 + + + + @@ -564,22 +613,6 @@ - - - - Qt::Vertical - - - QSizePolicy::MinimumExpanding - - - - 20 - 55 - - - - @@ -607,7 +640,8 @@ buttonBrowseFile challengeResponseCombo buttonRedetectYubikey - checkTouchID + quickUnlockButton + resetQuickUnlockButton diff --git a/src/gui/Icons.cpp b/src/gui/Icons.cpp index 73e48154d..0809e271b 100644 --- a/src/gui/Icons.cpp +++ b/src/gui/Icons.cpp @@ -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 diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 441dcd513..3bcda98d4 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -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& urls) diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index b2f4c11da..2fdc43ed1 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -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(); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 248f0b4d4..67dbd1f9a 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -16,7 +16,7 @@ 800 - 400 + 500 @@ -1091,18 +1091,18 @@ - - PasswordGeneratorWidget - QWidget -
gui/PasswordGeneratorWidget.h
- 1 -
MessageWidget QWidget
gui/MessageWidget.h
1
+ + PasswordGeneratorWidget + QWidget +
gui/PasswordGeneratorWidget.h
+ 1 +
DatabaseTabWidget QTabWidget diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index cefe75052..6d96dd769 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -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); } diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp index b518ee47d..2dae5cbb5 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp @@ -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 #include @@ -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(); diff --git a/src/gui/reports/ReportsDialog.cpp b/src/gui/reports/ReportsDialog.cpp index 406237459..e1da39839 100644 --- a/src/gui/reports/ReportsDialog.cpp +++ b/src/gui/reports/ReportsDialog.cpp @@ -121,14 +121,6 @@ void ReportsDialog::addPage(QSharedPointer 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); } diff --git a/src/gui/styles/base/basestyle.qss b/src/gui/styles/base/basestyle.qss index 545980f8d..3103b110b 100644 --- a/src/gui/styles/base/basestyle.qss +++ b/src/gui/styles/base/basestyle.qss @@ -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); } diff --git a/src/gui/styles/base/classicstyle.qss b/src/gui/styles/base/classicstyle.qss index 8ee51cf11..f7d3c0fb4 100644 --- a/src/gui/styles/base/classicstyle.qss +++ b/src/gui/styles/base/classicstyle.qss @@ -1,4 +1,4 @@ -DatabaseOpenWidget #loginFrame { +DatabaseOpenWidget #centralStack { border: 2px groove palette(mid); background: palette(light); } diff --git a/src/keys/CompositeKey.cpp b/src/keys/CompositeKey.cpp index 3fc990546..0fdb2b32f 100644 --- a/src/keys/CompositeKey.cpp +++ b/src/keys/CompositeKey.cpp @@ -209,7 +209,6 @@ QByteArray CompositeKey::serialize() const for (auto const& key : m_challengeResponseKeys) { stream << key->uuid().toRfc4122() << key->serialize(); } - return data; } diff --git a/src/touchid/TouchID.h b/src/touchid/TouchID.h index 27a07417e..a5e80f0f9 100644 --- a/src/touchid/TouchID.h +++ b/src/touchid/TouchID.h @@ -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; diff --git a/src/touchid/TouchID.mm b/src/touchid/TouchID.mm index 7fc9b54a8..236711e31 100644 --- a/src/touchid/TouchID.mm +++ b/src/touchid/TouchID.mm @@ -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. */ diff --git a/src/winhello/WindowsHello.cpp b/src/winhello/WindowsHello.cpp new file mode 100644 index 000000000..7095c72ee --- /dev/null +++ b/src/winhello/WindowsHello.cpp @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2022 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "WindowsHello.h" + +#include +#include +#include +#include +#include +#include + +#include "core/AsyncTask.h" +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" +#include "crypto/SymmetricCipher.h" + +#include +#include + +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(reinterpret_cast(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(response.data()), static_cast(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(); +} diff --git a/src/winhello/WindowsHello.h b/src/winhello/WindowsHello.h new file mode 100644 index 000000000..5faf7eb25 --- /dev/null +++ b/src/winhello/WindowsHello.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_WINDOWSHELLO_H +#define KEEPASSXC_WINDOWSHELLO_H + +#include +#include + +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 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