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