From d2ad2a95fe8197e37f5d1c692e2f6d0f5a44a318 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 5 Nov 2023 07:06:07 -0500 Subject: [PATCH 1/5] Add support to remember quick unlock on Windows and macOS --- src/core/Config.cpp | 2 +- src/core/Config.h | 2 +- src/gui/ApplicationSettingsWidget.cpp | 14 ++ src/gui/ApplicationSettingsWidgetGeneral.ui | 26 ++-- src/gui/ApplicationSettingsWidgetSecurity.ui | 34 ++++- src/gui/DatabaseOpenWidget.cpp | 5 +- src/gui/DatabaseWidget.cpp | 5 +- src/quickunlock/Polkit.cpp | 23 ++-- src/quickunlock/Polkit.h | 2 + src/quickunlock/QuickUnlockInterface.cpp | 5 + src/quickunlock/QuickUnlockInterface.h | 4 + src/quickunlock/TouchID.h | 2 + src/quickunlock/TouchID.mm | 130 +++++++------------ src/quickunlock/WindowsHello.cpp | 59 ++++++++- src/quickunlock/WindowsHello.h | 9 +- 15 files changed, 206 insertions(+), 116 deletions(-) diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 3f2636eff..f3fe5b917 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -155,7 +155,7 @@ static const QHash configStrings = { {Config::Security_NoConfirmMoveEntryToRecycleBin,{QS("Security/NoConfirmMoveEntryToRecycleBin"), Roaming, true}}, {Config::Security_EnableCopyOnDoubleClick,{QS("Security/EnableCopyOnDoubleClick"), Roaming, false}}, {Config::Security_QuickUnlock, {QS("Security/QuickUnlock"), Local, true}}, - {Config::Security_DatabasePasswordMinimumQuality, {QS("Security/DatabasePasswordMinimumQuality"), Local, 0}}, + {Config::Security_QuickUnlockRemember, {QS("Security/QuickUnlockRemember"), Local, true}}, // Browser {Config::Browser_Enabled, {QS("Browser/Enabled"), Roaming, false}}, diff --git a/src/core/Config.h b/src/core/Config.h index 8f54f9c01..5e4e1d691 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -136,7 +136,7 @@ public: Security_NoConfirmMoveEntryToRecycleBin, Security_EnableCopyOnDoubleClick, Security_QuickUnlock, - Security_DatabasePasswordMinimumQuality, + Security_QuickUnlockRemember, Browser_Enabled, Browser_ShowNotification, diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 9f24bbc85..97da17e97 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -129,6 +129,10 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) m_secUi->lockDatabaseMinimizeCheckBox->setEnabled(!state); }); + connect(m_secUi->quickUnlockCheckBox, &QCheckBox::toggled, this, [this](bool state) { + m_secUi->quickUnlockRememberCheckBox->setEnabled(state); + }); + // Set Auto-Type shortcut when changed connect( m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutChanged, this, [this](auto key, auto modifiers) { @@ -350,6 +354,15 @@ void ApplicationSettingsWidget::loadSettings() m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable()); m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool()); + m_secUi->quickUnlockCheckBox->setToolTip( + m_secUi->quickUnlockCheckBox->isEnabled() ? QString() : tr("Quick unlock is not available on your device.")); + + m_secUi->quickUnlockRememberCheckBox->setEnabled(getQuickUnlock()->isAvailable() + && getQuickUnlock()->canRemember()); + m_secUi->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool()); + m_secUi->quickUnlockRememberCheckBox->setToolTip(m_secUi->quickUnlockRememberCheckBox->isEnabled() + ? QString() + : tr("Quick unlock cannot be remembered on your device.")); for (const ExtraPage& page : asConst(m_extraPages)) { page.loadSettings(); @@ -471,6 +484,7 @@ void ApplicationSettingsWidget::saveSettings() if (m_secUi->quickUnlockCheckBox->isEnabled()) { config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked()); + config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked()); } // Security: clear storage if related settings are disabled diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 42ce4acc1..01472e6ba 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -148,7 +148,7 @@ 40 - 20 + 0 @@ -174,7 +174,7 @@ 30 - 20 + 0 @@ -210,7 +210,7 @@ 30 - 20 + 0 @@ -250,7 +250,7 @@ 30 - 20 + 0 @@ -315,7 +315,7 @@ 40 - 20 + 0 @@ -483,7 +483,7 @@ 30 - 20 + 0 @@ -516,7 +516,7 @@ 40 - 20 + 0 @@ -648,7 +648,7 @@ 30 - 20 + 0 @@ -681,7 +681,7 @@ 30 - 20 + 0 @@ -743,7 +743,7 @@ 40 - 20 + 0 @@ -971,7 +971,7 @@ 30 - 20 + 0 @@ -1019,7 +1019,7 @@ 40 - 20 + 0 @@ -1076,7 +1076,7 @@ 30 - 20 + 0 diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui index c4cb0e01f..aeafce324 100644 --- a/src/gui/ApplicationSettingsWidgetSecurity.ui +++ b/src/gui/ApplicationSettingsWidgetSecurity.ui @@ -138,7 +138,7 @@ 40 - 20 + 0 @@ -168,10 +168,40 @@ - Enable database quick unlock (Touch ID / Windows Hello) + Enable database quick unlock (Touch ID / Windows Hello / Polkit) + + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 30 + 0 + + + + + + + + Remember quick unlock after database is closed + + + + + diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 7cf5dcb15..f711b57d6 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -389,7 +389,10 @@ void DatabaseOpenWidget::openDatabase() // Save Quick Unlock credentials if available if (!blockQuickUnlock && isQuickUnlockAvailable()) { auto keyData = databaseKey->serialize(); - getQuickUnlock()->setKey(m_db->publicUuid(), keyData); + if (!getQuickUnlock()->setKey(m_db->publicUuid(), keyData) && !getQuickUnlock()->errorString().isEmpty()) { + getMainWindow()->displayTabMessage(getQuickUnlock()->errorString(), + MessageWidget::MessageType::Warning); + } m_ui->messageWidget->hideMessage(); } diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 2afde49fe..1ef9a47f9 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1979,7 +1979,10 @@ void DatabaseWidget::closeEvent(QCloseEvent* event) return; } - m_databaseOpenWidget->resetQuickUnlock(); + // Reset quick unlock if we are not remembering it + if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) { + m_databaseOpenWidget->resetQuickUnlock(); + } event->accept(); } diff --git a/src/quickunlock/Polkit.cpp b/src/quickunlock/Polkit.cpp index d73a7c71b..740ab12a8 100644 --- a/src/quickunlock/Polkit.cpp +++ b/src/quickunlock/Polkit.cpp @@ -110,14 +110,14 @@ bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key) SymmetricCipher aes256Encrypt; if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { - m_error = QObject::tr("AES initialization failed"); + m_error = QObject::tr("Failed to init KeePassXC crypto."); return false; } // Encrypt the master password QByteArray encryptedMasterKey = key; if (!aes256Encrypt.finish(encryptedMasterKey)) { - m_error = QObject::tr("AES encrypt failed"); + m_error = QObject::tr("Failed to encrypt key data."); qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString(); return false; } @@ -129,7 +129,7 @@ bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key) keychainKeyValue.size(), KEY_SPEC_PROCESS_KEYRING); if (key_serial < 0) { - m_error = QObject::tr("Failed to store in Linux Keyring"); + m_error = QObject::tr("Failed to store key in Linux Keyring. Quick unlock has not been enabled."); qDebug() << "polkit keyring failed to store: " << errno; return false; } @@ -181,7 +181,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING); if (keySerial == -1) { - m_error = QObject::tr("Could not locate key in keyring"); + m_error = QObject::tr("Could not locate key in Linux Keyring."); qDebug() << "polkit keyring failed to find: " << errno; return false; } @@ -190,7 +190,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer); if (keychainDataSize == -1) { - m_error = QObject::tr("Could not read key in keyring"); + m_error = QObject::tr("Could not read key in Linux Keyring."); qDebug() << "polkit keyring failed to read: " << errno; return false; } @@ -205,7 +205,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) SymmetricCipher aes256Decrypt; if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) { - m_error = QObject::tr("AES initialization failed"); + m_error = QObject::tr("Failed to init KeePassXC crypto."); qDebug() << "polkit aes init failed"; return false; } @@ -213,7 +213,7 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) key = encryptedMasterKey; if (!aes256Decrypt.finish(key)) { key.clear(); - m_error = QObject::tr("AES decrypt failed"); + m_error = QObject::tr("Failed to decrypt key data."); qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString(); return false; } @@ -229,14 +229,19 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) // Failed to authenticate if (authResult.is_challenge) { - m_error = QObject::tr("No Polkit authentication agent was available"); + m_error = QObject::tr("No Polkit authentication agent was available."); } else { - m_error = QObject::tr("Polkit authorization failed"); + m_error = QObject::tr("Polkit authorization failed."); } return false; } +bool Polkit::canRemember() const +{ + return false; +} + bool Polkit::hasKey(const QUuid& dbUuid) const { if (!m_encryptedMasterKeys.contains(dbUuid)) { diff --git a/src/quickunlock/Polkit.h b/src/quickunlock/Polkit.h index 7dfc2db7b..2dd8bbf6c 100644 --- a/src/quickunlock/Polkit.h +++ b/src/quickunlock/Polkit.h @@ -36,6 +36,8 @@ public: bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool hasKey(const QUuid& dbUuid) const override; + bool canRemember() const override; + void reset(const QUuid& dbUuid) override; void reset() override; diff --git a/src/quickunlock/QuickUnlockInterface.cpp b/src/quickunlock/QuickUnlockInterface.cpp index 0e24736e8..a04d7a1da 100644 --- a/src/quickunlock/QuickUnlockInterface.cpp +++ b/src/quickunlock/QuickUnlockInterface.cpp @@ -75,6 +75,11 @@ bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const return false; } +bool NoQuickUnlock::canRemember() const +{ + return false; +} + void NoQuickUnlock::reset(const QUuid& dbUuid) { Q_UNUSED(dbUuid) diff --git a/src/quickunlock/QuickUnlockInterface.h b/src/quickunlock/QuickUnlockInterface.h index 54aeb8a62..419fffe0a 100644 --- a/src/quickunlock/QuickUnlockInterface.h +++ b/src/quickunlock/QuickUnlockInterface.h @@ -35,6 +35,8 @@ public: virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0; virtual bool hasKey(const QUuid& dbUuid) const = 0; + virtual bool canRemember() const = 0; + virtual void reset(const QUuid& dbUuid) = 0; virtual void reset() = 0; }; @@ -49,6 +51,8 @@ public: bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool hasKey(const QUuid& dbUuid) const override; + bool canRemember() const override; + void reset(const QUuid& dbUuid) override; void reset() override; }; diff --git a/src/quickunlock/TouchID.h b/src/quickunlock/TouchID.h index 74e5d9474..ef4f5ebf3 100644 --- a/src/quickunlock/TouchID.h +++ b/src/quickunlock/TouchID.h @@ -31,6 +31,8 @@ public: bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override; bool hasKey(const QUuid& dbUuid) const override; + bool canRemember() const override; + void reset(const QUuid& dbUuid = "") override; void reset() override; diff --git a/src/quickunlock/TouchID.mm b/src/quickunlock/TouchID.mm index f7ea18014..2f6e4d092 100644 --- a/src/quickunlock/TouchID.mm +++ b/src/quickunlock/TouchID.mm @@ -68,7 +68,7 @@ void TouchID::deleteKeyEntry(const QString& accountName) // get data from the KeyChain OSStatus status = SecItemDelete(query); - LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status); + LogStatusError("TouchID::deleteKeyEntry - Error deleting existing entry", status); } QString TouchID::databaseKeyName(const QUuid& dbUuid) @@ -88,41 +88,20 @@ void TouchID::reset() m_encryptedMasterKeys.clear(); } - - - -bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID) +/** + * Store the serialized database key into the macOS key store. The OS handles encrypt/decrypt operations. + * https://developer.apple.com/documentation/security/keychain_services/keychain_items + */ +bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key, const bool ignoreTouchID) { - if (passwordKey.isEmpty()) { + if (key.isEmpty()) { debug("TouchID::setKey - illegal arguments"); return false; } - if (m_encryptedMasterKeys.contains(dbUuid)) { - debug("TouchID::setKey - Already stored key for this database"); - return true; - } - - // generate random AES 256bit key and IV - QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); - QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); - - SymmetricCipher aes256Encrypt; - if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { - debug("TouchID::setKey - AES initialisation failed"); - return false; - } - - // encrypt and keep result in memory - QByteArray encryptedMasterKey = passwordKey; - if (!aes256Encrypt.finish(encryptedMasterKey)) { - debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData()); - return false; - } - - const QString keyName = databaseKeyName(dbUuid); - - deleteKeyEntry(keyName); // Try to delete the existing key entry + const auto keyName = databaseKeyName(dbUuid); + // Try to delete the existing key entry + deleteKeyEntry(keyName); // prepare adding secure entry to the macOS KeyChain CFErrorRef error = NULL; @@ -168,30 +147,33 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const b kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error); if (sacObject == NULL || error != NULL) { - NSError* e = (__bridge NSError*) error; + auto e = (__bridge NSError*) error; debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String); return false; } - NSString *accountName = keyName.toNSString(); // The NSString is released by Qt + auto accountName = keyName.toNSString(); + auto keyBase64 = key.toBase64(); // prepare data (key) to be stored - QByteArray keychainKeyValue = (randomKey + randomIV).toHex(); - CFDataRef keychainValueData = - CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast(keychainKeyValue.data()), - keychainKeyValue.length(), kCFAllocatorDefault); + auto keyValueData = CFDataCreateWithBytesNoCopy( + kCFAllocatorDefault, reinterpret_cast(keyBase64.data()), + keyBase64.length(), kCFAllocatorDefault); - CFMutableDictionaryRef attributes = makeDictionary(); + auto attributes = makeDictionary(); CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword); CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName); - CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData); + CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keyValueData); CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse); CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow); +#ifndef QT_DEBUG + // Only use TouchID when in release build, also requires application entitlements and signing CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject); +#endif // add to KeyChain OSStatus status = SecItemAdd(attributes, NULL); - LogStatusError("TouchID::setKey - Status adding new entry", status); + LogStatusError("TouchID::setKey - Error adding new keychain item", status); CFRelease(sacObject); CFRelease(attributes); @@ -226,12 +208,12 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey) } /** - * Checks if an encrypted PasswordKey is available for the given database, tries to - * decrypt it using the KeyChain and if successful, returns it. + * Retrieve serialized key data from the macOS Keychain after successful authentication + * with TouchID or Watch interface. */ -bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey) +bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key) { - passwordKey.clear(); + key.clear(); if (!hasKey(dbUuid)) { debug("TouchID::getKey - No stored key found"); @@ -266,39 +248,30 @@ bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey) return false; } + // Convert value returned to serialized key CFDataRef valueData = static_cast(dataTypeRef); - QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast(CFDataGetBytePtr(valueData)), - CFDataGetLength(valueData))); + key = QByteArray::fromBase64(QByteArray(reinterpret_cast(CFDataGetBytePtr(valueData)), + CFDataGetLength(valueData))); CFRelease(dataTypeRef); - // extract AES key and IV from data bytes - QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); - QByteArray iv = dataBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); - - SymmetricCipher aes256Decrypt; - if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) { - debug("TouchID::getKey - AES initialization failed"); - return false; - } - - // decrypt PasswordKey from memory using AES - passwordKey = m_encryptedMasterKeys[dbUuid]; - if (!aes256Decrypt.finish(passwordKey)) { - passwordKey.clear(); - debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData()); - return false; - } - - // Cleanse the key information from the memory - Botan::secure_scrub_memory(key.data(), key.size()); - Botan::secure_scrub_memory(iv.data(), iv.size()); - return true; } bool TouchID::hasKey(const QUuid& dbUuid) const { - return m_encryptedMasterKeys.contains(dbUuid); + const QString keyName = databaseKeyName(dbUuid); + NSString* accountName = keyName.toNSString(); // The NSString is released by Qt + + CFMutableDictionaryRef query = makeDictionary(); + CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName); + CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse); + + CFTypeRef item = NULL; + OSStatus status = SecItemCopyMatching(query, &item); + CFRelease(query); + + return status == errSecSuccess; } // TODO: Both functions below should probably handle the returned errors to @@ -320,11 +293,7 @@ bool TouchID::isWatchAvailable() bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error]; [context release]; if (error) { - debug("Apple Wach available: %d (%ld / %s / %s)", canAuthenticate, - (long)error.code, error.description.UTF8String, - error.localizedDescription.UTF8String); - } else { - debug("Apple Wach available: %d", canAuthenticate); + debug("Apple Watch is not available: %s", error.localizedDescription.UTF8String); } return canAuthenticate; } @catch (NSException *) { @@ -348,11 +317,7 @@ bool TouchID::isTouchIdAvailable() bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error]; [context release]; if (error) { - debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate, - (long)error.code, error.description.UTF8String, - error.localizedDescription.UTF8String); - } else { - debug("Touch ID available: %d", canAuthenticate); + debug("Touch ID is not available: %s", error.localizedDescription.UTF8String); } return canAuthenticate; } @catch (NSException *) { @@ -399,10 +364,15 @@ bool TouchID::isAvailable() const return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible(); } +bool TouchID::canRemember() const +{ + return true; +} + /** * Resets the inner state either for all or for the given database */ void TouchID::reset(const QUuid& dbUuid) { - m_encryptedMasterKeys.remove(dbUuid); + deleteKeyEntry(databaseKeyName(dbUuid)); } diff --git a/src/quickunlock/WindowsHello.cpp b/src/quickunlock/WindowsHello.cpp index 890e3499a..b1d7c94b2 100644 --- a/src/quickunlock/WindowsHello.cpp +++ b/src/quickunlock/WindowsHello.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -34,6 +35,7 @@ using namespace winrt; using namespace Windows::Foundation; +using namespace Windows::Foundation::Collections; using namespace Windows::Security::Credentials; using namespace Windows::Security::Cryptography; using namespace Windows::Storage::Streams; @@ -97,6 +99,47 @@ namespace } }); } + + void storeCredential(const QUuid& uuid, const QByteArray& data) + { + auto vault = PasswordVault(); + vault.Add({s_winHelloKeyName, + winrt::to_hstring(uuid.toString().toStdString()), + winrt::to_hstring(data.toBase64().toStdString())}); + } + + void removeCredential(const QUuid& uuid) + { + try { + auto vault = PasswordVault(); + vault.Remove({s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString()), L"blah"}); + } catch (winrt::hresult_error const& ex) { + } + } + + void resetCredentials() + { + auto vault = PasswordVault(); + auto credentials = vault.FindAllByResource(s_winHelloKeyName); + for (const auto& credential : credentials) { + try { + vault.Remove(credential); + } catch (winrt::hresult_error const& ex) { + } + } + } + + QByteArray loadCredential(const QUuid& uuid) + { + QByteArray data; + try { + auto vault = PasswordVault(); + auto credential = vault.Retrieve(s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString())); + data = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password()))); + } catch (winrt::hresult_error const& ex) { + } + return data; + } } // namespace bool WindowsHello::isAvailable() const @@ -120,6 +163,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data) auto challenge = Random::instance()->randomArray(ivSize); QByteArray key; if (!deriveEncryptionKey(challenge, key, m_error)) { + m_error = QObject::tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled."); return false; } @@ -137,7 +181,7 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data) // Prepend the challenge/IV to the encrypted data encrypted.prepend(challenge); - m_encryptedKeys.insert(dbUuid, encrypted); + storeCredential(dbUuid, encrypted); return true; } @@ -153,7 +197,7 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data) // Read the previously used challenge and encrypted data auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); - const auto& keydata = m_encryptedKeys.value(dbUuid); + const auto& keydata = loadCredential(dbUuid); auto challenge = keydata.left(ivSize); auto encrypted = keydata.mid(ivSize); QByteArray key; @@ -182,15 +226,20 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data) void WindowsHello::reset(const QUuid& dbUuid) { - m_encryptedKeys.remove(dbUuid); + removeCredential(dbUuid); } bool WindowsHello::hasKey(const QUuid& dbUuid) const { - return m_encryptedKeys.contains(dbUuid); + return !loadCredential(dbUuid).isEmpty(); +} + +bool WindowsHello::canRemember() const +{ + return true; } void WindowsHello::reset() { - m_encryptedKeys.clear(); + resetCredentials(); } diff --git a/src/quickunlock/WindowsHello.h b/src/quickunlock/WindowsHello.h index 9da6e4160..3032b89a9 100644 --- a/src/quickunlock/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -27,18 +27,21 @@ class WindowsHello : public QuickUnlockInterface { public: WindowsHello() = default; + bool isAvailable() const override; QString errorString() const override; - void reset() override; bool setKey(const QUuid& dbUuid, const QByteArray& key) override; bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool hasKey(const QUuid& dbUuid) const override; - void reset(const QUuid& dbUuid) override; + bool canRemember() const override; + + void reset(const QUuid& dbUuid) override; + void reset() override; + private: QString m_error; - QHash m_encryptedKeys; Q_DISABLE_COPY(WindowsHello); }; From 656e0c71a3c66b3d401e450b508f829295541ee8 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 1 Dec 2024 23:46:56 -0500 Subject: [PATCH 2/5] Add Pin Quick Unlock option * Introduce QuickUnlockManager to fall back to pin unlock if OS native options are not available. --- share/translations/keepassxc_en.ts | 122 ++++++++----- src/CMakeLists.txt | 1 + src/core/Config.h | 1 + src/gui/ApplicationSettingsWidget.cpp | 23 +-- src/gui/ApplicationSettingsWidgetSecurity.ui | 40 +--- src/gui/DatabaseOpenDialog.cpp | 5 +- src/gui/DatabaseOpenWidget.cpp | 97 +++++----- src/gui/DatabaseOpenWidget.h | 21 ++- src/gui/DatabaseOpenWidget.ui | 116 +++++++++++- src/gui/DatabaseWidget.cpp | 5 - .../DatabaseSettingsWidgetDatabaseKey.cpp | 2 +- src/quickunlock/PinUnlock.cpp | 171 ++++++++++++++++++ src/quickunlock/PinUnlock.h | 49 +++++ src/quickunlock/QuickUnlockInterface.cpp | 70 ++++--- src/quickunlock/QuickUnlockInterface.h | 24 +-- src/quickunlock/TouchID.mm | 9 +- src/quickunlock/WindowsHello.h | 8 +- 17 files changed, 536 insertions(+), 228 deletions(-) create mode 100644 src/quickunlock/PinUnlock.cpp create mode 100644 src/quickunlock/PinUnlock.h diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index ffc840a4f..345a6b7f9 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -657,10 +657,6 @@ Convenience - - Enable database quick unlock (Touch ID / Windows Hello) - - Lock databases when session is locked or lid is closed @@ -705,6 +701,18 @@ Hide notes in the entry preview panel + + Quick unlock can only be remembered when using Touch ID or Windows Hello + + + + Enable database quick unlock by default + + + + Remember quick unlock after database is closed (Touch ID / Windows Hello only) + + AttachmentWidget @@ -1654,10 +1662,6 @@ Backup database located at %2 Unlock Database - - Cancel - - Unlock @@ -1791,6 +1795,18 @@ Are you sure you want to continue with this file?. <a href="#" style="text-decoration: underline">I have a key file</a> + + Enable Quick Unlock + + + + Reset + + + + Close Database + + Hardware keys found, but no slots are configured. @@ -9126,46 +9142,10 @@ This option is deprecated, use --set-key-file instead. Passkeys - - AES initialization failed - - - - AES encrypt failed - - - - Failed to store in Linux Keyring - - Polkit returned an error: %1 - - Could not locate key in keyring - - - - Could not read key in keyring - - - - AES decrypt failed - - - - No Polkit authentication agent was available - - - - Polkit authorization failed - - - - No Quick Unlock provider is available - - Failed to init KeePassXC crypto. @@ -9341,7 +9321,55 @@ This option is deprecated, use --set-key-file instead. - Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'. + Quick Unlock Pin Entry + + + + Enter a %1 to %2 digit pin to use for quick unlock: + + + + Pin setup was canceled. Quick unlock has not been enabled. + + + + Failed to get credentials for quick unlock. + + + + Enter quick unlock pin (%1 of %2 attempts): + + + + Pin entry was canceled. + + + + Maximum pin attempts have been reached. + + + + Failed to store key in Linux Keyring. Quick unlock has not been enabled. + + + + Could not locate key in Linux Keyring. + + + + Could not read key in Linux Keyring. + + + + No Polkit authentication agent was available. + + + + Polkit authorization failed. + + + + Windows Hello setup was canceled or failed. Quick unlock has not been enabled. @@ -9425,6 +9453,10 @@ This option is deprecated, use --set-key-file instead. Confirm Replace Entry References + + Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'. + + QtIOCompressor diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fcc29784d..d12da9dc0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -217,6 +217,7 @@ set(gui_SOURCES gui/wizard/NewDatabaseWizardPageEncryption.cpp gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp quickunlock/QuickUnlockInterface.cpp + quickunlock/PinUnlock.cpp ../share/icons/icons.qrc ../share/wizard/wizard.qrc) diff --git a/src/core/Config.h b/src/core/Config.h index 5e4e1d691..7652caecc 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -137,6 +137,7 @@ public: Security_EnableCopyOnDoubleClick, Security_QuickUnlock, Security_QuickUnlockRemember, + Security_DatabasePasswordMinimumQuality, Browser_Enabled, Browser_ShowNotification, diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 97da17e97..aa3fa2471 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -129,10 +129,6 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) m_secUi->lockDatabaseMinimizeCheckBox->setEnabled(!state); }); - connect(m_secUi->quickUnlockCheckBox, &QCheckBox::toggled, this, [this](bool state) { - m_secUi->quickUnlockRememberCheckBox->setEnabled(state); - }); - // Set Auto-Type shortcut when changed connect( m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutChanged, this, [this](auto key, auto modifiers) { @@ -352,17 +348,12 @@ void ApplicationSettingsWidget::loadSettings() m_secUi->hideTotpCheckBox->setChecked(config()->get(Config::Security_HideTotpPreviewPanel).toBool()); m_secUi->hideNotesCheckBox->setChecked(config()->get(Config::Security_HideNotes).toBool()); - m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable()); m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool()); - m_secUi->quickUnlockCheckBox->setToolTip( - m_secUi->quickUnlockCheckBox->isEnabled() ? QString() : tr("Quick unlock is not available on your device.")); - - m_secUi->quickUnlockRememberCheckBox->setEnabled(getQuickUnlock()->isAvailable() - && getQuickUnlock()->canRemember()); m_secUi->quickUnlockRememberCheckBox->setChecked(config()->get(Config::Security_QuickUnlockRemember).toBool()); - m_secUi->quickUnlockRememberCheckBox->setToolTip(m_secUi->quickUnlockRememberCheckBox->isEnabled() - ? QString() - : tr("Quick unlock cannot be remembered on your device.")); +#ifdef Q_OS_LINUX + // Remembering quick unlock is not supported on Linux + m_secUi->quickUnlockRememberCheckBox->setVisible(false); +#endif for (const ExtraPage& page : asConst(m_extraPages)) { page.loadSettings(); @@ -482,10 +473,8 @@ void ApplicationSettingsWidget::saveSettings() config()->set(Config::Security_HideTotpPreviewPanel, m_secUi->hideTotpCheckBox->isChecked()); config()->set(Config::Security_HideNotes, m_secUi->hideNotesCheckBox->isChecked()); - if (m_secUi->quickUnlockCheckBox->isEnabled()) { - config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked()); - config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked()); - } + config()->set(Config::Security_QuickUnlock, m_secUi->quickUnlockCheckBox->isChecked()); + config()->set(Config::Security_QuickUnlockRemember, m_secUi->quickUnlockRememberCheckBox->isChecked()); // Security: clear storage if related settings are disabled if (!config()->get(Config::RememberLastDatabases).toBool()) { diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui index aeafce324..f708995a5 100644 --- a/src/gui/ApplicationSettingsWidgetSecurity.ui +++ b/src/gui/ApplicationSettingsWidgetSecurity.ui @@ -6,8 +6,8 @@ 0 0 - 364 - 505 + 437 + 529 @@ -168,39 +168,19 @@ - Enable database quick unlock (Touch ID / Windows Hello / Polkit) + Enable database quick unlock by default - - - 0 + + + Quick unlock can only be remembered when using Touch ID or Windows Hello - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 30 - 0 - - - - - - - - Remember quick unlock after database is closed - - - - + + Remember quick unlock after database is closed (Touch ID / Windows Hello only) + + diff --git a/src/gui/DatabaseOpenDialog.cpp b/src/gui/DatabaseOpenDialog.cpp index 881db4087..70ede8986 100644 --- a/src/gui/DatabaseOpenDialog.cpp +++ b/src/gui/DatabaseOpenDialog.cpp @@ -84,9 +84,8 @@ void DatabaseOpenDialog::showEvent(QShowEvent* event) { QDialog::showEvent(event); QTimer::singleShot(100, this, [this] { - if (m_view->isOnQuickUnlockScreen() && !m_view->unlockingDatabase()) { - m_view->triggerQuickUnlock(); - } + // Automatically trigger quick unlock if it's available + m_view->triggerQuickUnlock(); }); } diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index f711b57d6..eef4c2899 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -38,14 +38,6 @@ namespace { constexpr int clearFormsDelay = 30000; - - bool isQuickUnlockAvailable() - { - if (config()->get(Config::Security_QuickUnlock).toBool()) { - return getQuickUnlock()->isAvailable(); - } - return false; - } } // namespace DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) @@ -68,17 +60,10 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) m_ui->editPassword->setShowPassword(false); }); - QFont font; - font.setPointSize(font.pointSize() + 4); - font.setBold(true); - m_ui->labelHeadline->setFont(font); - - m_ui->quickUnlockButton->setFont(font); - m_ui->quickUnlockButton->setIcon( - icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText))); - m_ui->quickUnlockButton->setIconSize({32, 32}); - - connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile())); + QFont largeFont; + largeFont.setPointSize(largeFont.pointSize() + 4); + largeFont.setBold(true); + m_ui->labelHeadline->setFont(largeFont); auto okBtn = m_ui->buttonBox->button(QDialogButtonBox::Ok); okBtn->setText(tr("Unlock")); @@ -86,16 +71,19 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase())); connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject())); + // Key file components + m_ui->selectKeyFileComponent->setVisible(false); connect(m_ui->addKeyFileLinkLabel, &QLabel::linkActivated, this, &DatabaseOpenWidget::browseKeyFile); + connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile())); connect(m_ui->keyFileLineEdit, &PasswordWidget::textChanged, this, [&](const QString& text) { bool state = !text.isEmpty(); m_ui->addKeyFileLinkLabel->setVisible(!state); m_ui->selectKeyFileComponent->setVisible(state); }); - connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled); - m_ui->selectKeyFileComponent->setVisible(false); + // Hardware key components toggleHardwareKeyComponent(false); + connect(m_ui->useHardwareKeyCheckBox, &QCheckBox::toggled, m_ui->hardwareKeyCombo, &QComboBox::setEnabled); QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy(); sp.setRetainSizeWhenHidden(true); @@ -127,13 +115,24 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) m_ui->refreshHardwareKeys->setVisible(false); #endif - // QuickUnlock actions + // QuickUnlock components + m_ui->quickUnlockButton->setFont(largeFont); + m_ui->quickUnlockButton->setIcon( + icons()->icon("fingerprint", true, palette().color(QPalette::Active, QPalette::HighlightedText))); + connect(m_ui->quickUnlockButton, &QPushButton::pressed, this, [this] { openDatabase(); }); connect(m_ui->resetQuickUnlockButton, &QPushButton::pressed, this, [this] { resetQuickUnlock(); }); + connect(m_ui->closeQuickUnlockButton, &QPushButton::pressed, this, [this] { reject(); }); m_ui->resetQuickUnlockButton->setShortcut(Qt::Key_Escape); } -DatabaseOpenWidget::~DatabaseOpenWidget() = default; +DatabaseOpenWidget::~DatabaseOpenWidget() +{ + // Reset quick unlock if we are not remembering it + if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) { + resetQuickUnlock(); + } +} void DatabaseOpenWidget::toggleHardwareKeyComponent(bool state) { @@ -189,7 +188,7 @@ bool DatabaseOpenWidget::event(QEvent* event) auto type = event->type(); if (type == QEvent::Show || type == QEvent::WindowActivate) { - if (isOnQuickUnlockScreen() && (m_db.isNull() || !canPerformQuickUnlock())) { + if (isOnQuickUnlockScreen() && !canPerformQuickUnlock()) { resetQuickUnlock(); } toggleQuickUnlockScreen(); @@ -294,6 +293,7 @@ void DatabaseOpenWidget::load(const QString& filename) } toggleQuickUnlockScreen(); + m_ui->enableQuickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool()); #ifdef WITH_XC_YUBIKEY // Do initial auto-poll @@ -335,16 +335,12 @@ void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile) m_ui->editPassword->setText(pw); m_ui->keyFileLineEdit->setText(keyFile); - m_blockQuickUnlock = true; + m_ui->enableQuickUnlockCheckBox->setChecked(false); openDatabase(); } void DatabaseOpenWidget::openDatabase() { - // Cache this variable for future use then reset - bool blockQuickUnlock = m_blockQuickUnlock || isOnQuickUnlockScreen(); - m_blockQuickUnlock = false; - setUserInteractionLock(true); m_ui->editPassword->setShowPassword(false); m_ui->messageWidget->hide(); @@ -386,12 +382,12 @@ void DatabaseOpenWidget::openDatabase() } } - // Save Quick Unlock credentials if available - if (!blockQuickUnlock && isQuickUnlockAvailable()) { + // Save Quick Unlock credentials if available and enabled + if (!isOnQuickUnlockScreen() && isQuickUnlockAvailable() && m_ui->enableQuickUnlockCheckBox->isChecked()) { auto keyData = databaseKey->serialize(); - if (!getQuickUnlock()->setKey(m_db->publicUuid(), keyData) && !getQuickUnlock()->errorString().isEmpty()) { - getMainWindow()->displayTabMessage(getQuickUnlock()->errorString(), - MessageWidget::MessageType::Warning); + auto qu = getQuickUnlock()->interface(); + if (!qu->setKey(m_db->publicUuid(), keyData) && !qu->errorString().isEmpty()) { + getMainWindow()->displayTabMessage(qu->errorString(), MessageWidget::MessageType::Warning); } m_ui->messageWidget->hideMessage(); } @@ -437,13 +433,16 @@ QSharedPointer DatabaseOpenWidget::buildDatabaseKey() { auto databaseKey = QSharedPointer::create(); - if (!m_db.isNull() && canPerformQuickUnlock()) { - // try to retrieve the stored password using Windows Hello + if (canPerformQuickUnlock()) { + // try to retrieve the stored password using quick unlock QByteArray keyData; - if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) { - m_ui->messageWidget->showMessage( - tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()), - MessageWidget::Error); + auto qu = getQuickUnlock()->interface(); + if (!qu->getKey(m_db->publicUuid(), keyData)) { + m_ui->messageWidget->showMessage(tr("Failed to authenticate with Quick Unlock: %1").arg(qu->errorString()), + MessageWidget::Error); + if (!qu->hasKey(m_db->publicUuid())) { + resetQuickUnlock(); + } return {}; } databaseKey->setRawKey(keyData); @@ -630,9 +629,15 @@ void DatabaseOpenWidget::setUserInteractionLock(bool state) m_unlockingDatabase = state; } +bool DatabaseOpenWidget::isQuickUnlockAvailable() const +{ + auto qu = getQuickUnlock()->interface(); + return qu && qu->isAvailable(); +} + bool DatabaseOpenWidget::canPerformQuickUnlock() const { - return !m_db.isNull() && isQuickUnlockAvailable() && getQuickUnlock()->hasKey(m_db->publicUuid()); + return m_db && isQuickUnlockAvailable() && getQuickUnlock()->interface()->hasKey(m_db->publicUuid()); } bool DatabaseOpenWidget::isOnQuickUnlockScreen() const @@ -659,7 +664,7 @@ void DatabaseOpenWidget::toggleQuickUnlockScreen() void DatabaseOpenWidget::triggerQuickUnlock() { - if (isOnQuickUnlockScreen()) { + if (isOnQuickUnlockScreen() && !unlockingDatabase()) { m_ui->quickUnlockButton->click(); } } @@ -671,11 +676,9 @@ void DatabaseOpenWidget::triggerQuickUnlock() */ void DatabaseOpenWidget::resetQuickUnlock() { - if (!isQuickUnlockAvailable()) { - return; - } - if (!m_db.isNull()) { - getQuickUnlock()->reset(m_db->publicUuid()); + auto qu = getQuickUnlock()->interface(); + if (m_db && qu) { + qu->reset(m_db->publicUuid()); } load(m_filename); } diff --git a/src/gui/DatabaseOpenWidget.h b/src/gui/DatabaseOpenWidget.h index d730634b3..70046ecd4 100644 --- a/src/gui/DatabaseOpenWidget.h +++ b/src/gui/DatabaseOpenWidget.h @@ -19,7 +19,6 @@ #ifndef KEEPASSX_DATABASEOPENWIDGET_H #define KEEPASSX_DATABASEOPENWIDGET_H -#include #include #include @@ -46,21 +45,17 @@ class DatabaseOpenWidget : public DialogyWidget public: explicit DatabaseOpenWidget(QWidget* parent = nullptr); ~DatabaseOpenWidget() override; + void load(const QString& filename); QString filename(); + QSharedPointer database(); + void clearForms(); void enterKey(const QString& pw, const QString& keyFile); - QSharedPointer database(); + void triggerQuickUnlock(); bool unlockingDatabase(); void showMessage(const QString& text, MessageWidget::MessageType type, int autoHideTimeout); - // Quick Unlock helper functions - bool canPerformQuickUnlock() const; - bool isOnQuickUnlockScreen() const; - void toggleQuickUnlockScreen(); - void triggerQuickUnlock(); - void resetQuickUnlock(); - signals: void dialogFinished(bool accepted); @@ -85,14 +80,20 @@ private slots: void closeDatabase(); void pollHardwareKey(bool manualTrigger = false, int delay = 0); void hardwareKeyResponse(bool found); + void resetQuickUnlock(); private: + // Quick Unlock helper functions + bool isQuickUnlockAvailable() const; + bool canPerformQuickUnlock() const; + bool isOnQuickUnlockScreen() const; + void toggleQuickUnlockScreen(); + #ifdef WITH_XC_YUBIKEY QPointer m_deviceListener; #endif bool m_pollingHardwareKey = false; bool m_manualHardwareKeyRefresh = false; - bool m_blockQuickUnlock = false; bool m_unlockingDatabase = false; bool m_triedToQuit = false; QTimer m_hideTimer; diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index 1ef04a528..eef43dd27 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -180,7 +180,7 @@ 0 - 10 + 0 @@ -192,7 +192,7 @@ - 2 + 0 @@ -250,13 +250,13 @@ 0 - 0 + 10 0 - 10 + 15 @@ -399,7 +399,7 @@ 40 - 20 + 0 @@ -465,6 +465,48 @@ 5 + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::RightToLeft + + + Enable Quick Unlock + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 8 + 20 + + + + @@ -511,6 +553,9 @@ + + 0 + @@ -542,17 +587,69 @@ Unlock Database + + + 32 + 32 + + true - - - Cancel + + + Qt::Vertical - + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + 0 + + + + + Reset + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 6 + 20 + + + + + + + + Close Database + + + + @@ -646,7 +743,6 @@ quickUnlockButton - resetQuickUnlockButton editPassword keyFileLineEdit buttonBrowseFile diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 1ef9a47f9..d8290705e 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1978,11 +1978,6 @@ void DatabaseWidget::closeEvent(QCloseEvent* event) event->ignore(); return; } - - // Reset quick unlock if we are not remembering it - if (!config()->get(Config::Security_QuickUnlockRemember).toBool()) { - m_databaseOpenWidget->resetQuickUnlock(); - } event->accept(); } diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp index a74b20ead..7d6081bcc 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp @@ -229,7 +229,7 @@ bool DatabaseSettingsWidgetDatabaseKey::saveSettings() m_db->setKey(newKey, true, false, false); - getQuickUnlock()->reset(m_db->publicUuid()); + getQuickUnlock()->interface()->reset(m_db->publicUuid()); emit editFinished(true); if (m_isDirty) { diff --git a/src/quickunlock/PinUnlock.cpp b/src/quickunlock/PinUnlock.cpp new file mode 100644 index 000000000..54d7ef1ee --- /dev/null +++ b/src/quickunlock/PinUnlock.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2023 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 "PinUnlock.h" + +#include "crypto/CryptoHash.h" +#include "crypto/Random.h" +#include "crypto/SymmetricCipher.h" + +#include +#include + +#define MIN_PIN_LENGTH 4 +#define MAX_PIN_LENGTH 8 +#define MAX_PIN_ATTEMPTS 3 + +bool PinUnlock::isAvailable() const +{ + return true; +} + +QString PinUnlock::errorString() const +{ + return m_error; +} + +bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data) +{ + QString pin; + QRegularExpression pinRegex("^\\d+$"); + while (true) { + bool ok = false; + pin = QInputDialog::getText( + nullptr, + QObject::tr("Quick Unlock Pin Entry"), + QObject::tr("Enter a %1 to %2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH), + QLineEdit::Password, + {}, + &ok); + + if (!ok) { + m_error = QObject::tr("Pin setup was canceled. Quick unlock has not been enabled."); + return false; + } + + // Validate pin criteria + if (pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH && pinRegex.match(pin).hasMatch()) { + break; + } + } + + // Hash the pin and use it as the key for the encryption + CryptoHash hash(CryptoHash::Sha256); + hash.addData(pin.toLatin1()); + auto key = hash.result(); + + // Generate a random IV + auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); + + // Encrypt the data using AES-256-CBC + SymmetricCipher cipher; + if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) { + m_error = QObject::tr("Failed to init KeePassXC crypto."); + return false; + } + QByteArray encrypted = data; + if (!cipher.finish(encrypted)) { + m_error = QObject::tr("Failed to encrypt key data."); + return false; + } + + // Prepend the IV to the encrypted data + encrypted.prepend(iv); + // Store the encrypted data and pin attempts + m_encryptedKeys.insert(dbUuid, qMakePair(1, encrypted)); + + return true; +} + +bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data) +{ + data.clear(); + if (!hasKey(dbUuid)) { + m_error = QObject::tr("Failed to get credentials for quick unlock."); + return false; + } + + const auto& pairData = m_encryptedKeys.value(dbUuid); + + // Restrict pin attempts per database + for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) { + bool ok = false; + auto pin = QInputDialog::getText( + nullptr, + QObject::tr("Quick Unlock Pin Entry"), + QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(pinAttempts).arg(MAX_PIN_ATTEMPTS), + QLineEdit::Password, + {}, + &ok); + + if (!ok) { + m_error = QObject::tr("Pin entry was canceled."); + return false; + } + + // Hash the pin and use it as the key for the encryption + CryptoHash hash(CryptoHash::Sha256); + hash.addData(pin.toLatin1()); + auto key = hash.result(); + + // Read the previously used challenge and encrypted data + auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); + const auto& keydata = pairData.second; + auto challenge = keydata.left(ivSize); + auto encrypted = keydata.mid(ivSize); + + // 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 = QObject::tr("Failed to init KeePassXC crypto."); + return false; + } + + // Store the decrypted data into the passed parameter + data = encrypted; + if (cipher.finish(data)) { + // Reset the pin attempts + m_encryptedKeys.insert(dbUuid, qMakePair(1, keydata)); + return true; + } + } + + data.clear(); + m_error = QObject::tr("Maximum pin attempts have been reached."); + reset(dbUuid); + return false; +} + +bool PinUnlock::hasKey(const QUuid& dbUuid) const +{ + return m_encryptedKeys.contains(dbUuid); +} + +bool PinUnlock::canRemember() const +{ + return false; +} + +void PinUnlock::reset(const QUuid& dbUuid) +{ + m_encryptedKeys.remove(dbUuid); +} + +void PinUnlock::reset() +{ + m_encryptedKeys.clear(); +} diff --git a/src/quickunlock/PinUnlock.h b/src/quickunlock/PinUnlock.h new file mode 100644 index 000000000..c285ad8e9 --- /dev/null +++ b/src/quickunlock/PinUnlock.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 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_PINUNLOCK_H +#define KEEPASSXC_PINUNLOCK_H + +#include "QuickUnlockInterface.h" + +#include + +class PinUnlock : public QuickUnlockInterface +{ +public: + PinUnlock() = default; + + bool isAvailable() const override; + QString errorString() const override; + + bool setKey(const QUuid& dbUuid, const QByteArray& key) override; + bool getKey(const QUuid& dbUuid, QByteArray& key) override; + bool hasKey(const QUuid& dbUuid) const override; + + bool canRemember() const override; + + void reset(const QUuid& dbUuid) override; + void reset() override; + +private: + QString m_error; + QHash> m_encryptedKeys; + + Q_DISABLE_COPY(PinUnlock) +}; + +#endif // KEEPASSXC_PINUNLOCK_H diff --git a/src/quickunlock/QuickUnlockInterface.cpp b/src/quickunlock/QuickUnlockInterface.cpp index a04d7a1da..2e55dd41f 100644 --- a/src/quickunlock/QuickUnlockInterface.cpp +++ b/src/quickunlock/QuickUnlockInterface.cpp @@ -16,71 +16,63 @@ */ #include "QuickUnlockInterface.h" +#include "PinUnlock.h" + #include #if defined(Q_OS_MACOS) #include "TouchID.h" -#define QUICKUNLOCK_IMPLEMENTATION TouchID #elif defined(Q_CC_MSVC) #include "WindowsHello.h" -#define QUICKUNLOCK_IMPLEMENTATION WindowsHello #elif defined(Q_OS_LINUX) #include "Polkit.h" -#define QUICKUNLOCK_IMPLEMENTATION Polkit -#else -#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock #endif -QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr}; +QuickUnlockManager* g_quickUnlockManager = nullptr; -QuickUnlockInterface* getQuickUnlock() +QuickUnlockManager* getQuickUnlock() { - if (!quickUnlockInstance) { - quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION(); + if (!g_quickUnlockManager) { + g_quickUnlockManager = new QuickUnlockManager(); } - return quickUnlockInstance; + return g_quickUnlockManager; } -bool NoQuickUnlock::isAvailable() const +QuickUnlockManager::QuickUnlockManager() { - return false; + // Create the native interface based on the platform +#if defined(Q_OS_MACOS) + m_nativeInterface.reset(new TouchID()); +#elif defined(Q_CC_MSVC) + m_nativeInterface.reset(new WindowsHello()); +#elif defined(Q_OS_LINUX) + m_nativeInterface.reset(new Polkit()); +#endif + // Always create the fallback interface + m_fallbackInterface.reset(new PinUnlock()); } -QString NoQuickUnlock::errorString() const -{ - return QObject::tr("No Quick Unlock provider is available"); -} - -void NoQuickUnlock::reset() +QuickUnlockManager::~QuickUnlockManager() { } -bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key) +QSharedPointer QuickUnlockManager::interface() const { - Q_UNUSED(dbUuid) - Q_UNUSED(key) - return false; + if (isNativeAvailable()) { + return m_nativeInterface; + } + return m_fallbackInterface; } -bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key) +bool QuickUnlockManager::isNativeAvailable() const { - Q_UNUSED(dbUuid) - Q_UNUSED(key) - return false; + return m_nativeInterface && m_nativeInterface->isAvailable(); } -bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const +bool QuickUnlockManager::isRememberAvailable() const { - Q_UNUSED(dbUuid) - return false; -} - -bool NoQuickUnlock::canRemember() const -{ - return false; -} - -void NoQuickUnlock::reset(const QUuid& dbUuid) -{ - Q_UNUSED(dbUuid) + if (isNativeAvailable()) { + return m_nativeInterface->canRemember(); + } + return m_fallbackInterface->canRemember(); } diff --git a/src/quickunlock/QuickUnlockInterface.h b/src/quickunlock/QuickUnlockInterface.h index 419fffe0a..df55c01a8 100644 --- a/src/quickunlock/QuickUnlockInterface.h +++ b/src/quickunlock/QuickUnlockInterface.h @@ -18,6 +18,7 @@ #ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H #define KEEPASSXC_QUICKUNLOCKINTERFACE_H +#include #include class QuickUnlockInterface @@ -41,22 +42,23 @@ public: virtual void reset() = 0; }; -class NoQuickUnlock : public QuickUnlockInterface +class QuickUnlockManager final { + Q_DISABLE_COPY(QuickUnlockManager) + public: - bool isAvailable() const override; - QString errorString() const override; + QuickUnlockManager(); + ~QuickUnlockManager(); - bool setKey(const QUuid& dbUuid, const QByteArray& key) override; - bool getKey(const QUuid& dbUuid, QByteArray& key) override; - bool hasKey(const QUuid& dbUuid) const override; + QSharedPointer interface() const; + bool isNativeAvailable() const; + bool isRememberAvailable() const; - bool canRemember() const override; - - void reset(const QUuid& dbUuid) override; - void reset() override; +private: + QSharedPointer m_nativeInterface; + QSharedPointer m_fallbackInterface; }; -QuickUnlockInterface* getQuickUnlock(); +QuickUnlockManager* getQuickUnlock(); #endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H diff --git a/src/quickunlock/TouchID.mm b/src/quickunlock/TouchID.mm index 2f6e4d092..60aa45cb7 100644 --- a/src/quickunlock/TouchID.mm +++ b/src/quickunlock/TouchID.mm @@ -85,6 +85,7 @@ QString TouchID::errorString() const void TouchID::reset() { + // TODO: Clear all credentials associated with KeePassXC m_encryptedMasterKeys.clear(); } @@ -177,17 +178,15 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key, const bool igno CFRelease(sacObject); CFRelease(attributes); - - // Cleanse the key information from the memory - Botan::secure_scrub_memory(randomKey.data(), randomKey.size()); - Botan::secure_scrub_memory(randomIV.data(), randomIV.size()); + // Cleanse the key information from the memory if (status != errSecSuccess) { return false; } // memorize which database the stored key is for - m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); + // TODO: Do we need to store the db uuid's to do a full reset later? + //m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); debug("TouchID::setKey - Success!"); return true; } diff --git a/src/quickunlock/WindowsHello.h b/src/quickunlock/WindowsHello.h index 3032b89a9..67f504bb2 100644 --- a/src/quickunlock/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -20,9 +20,6 @@ #include "QuickUnlockInterface.h" -#include -#include - class WindowsHello : public QuickUnlockInterface { public: @@ -39,10 +36,11 @@ public: void reset(const QUuid& dbUuid) override; void reset() override; - + private: QString m_error; - Q_DISABLE_COPY(WindowsHello); + + Q_DISABLE_COPY(WindowsHello) }; #endif // KEEPASSXC_WINDOWSHELLO_H From 4e2c06b94346bc59abb7a2ce904d2a4f870d1fb5 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 12 Jul 2025 12:47:14 -0400 Subject: [PATCH 3/5] Add safeguard to using Argon2 function --- src/crypto/kdf/Argon2Kdf.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/crypto/kdf/Argon2Kdf.cpp b/src/crypto/kdf/Argon2Kdf.cpp index f7b028ffe..233463d57 100644 --- a/src/crypto/kdf/Argon2Kdf.cpp +++ b/src/crypto/kdf/Argon2Kdf.cpp @@ -163,6 +163,13 @@ QVariantMap Argon2Kdf::writeParameters() bool Argon2Kdf::transform(const QByteArray& raw, QByteArray& result) const { + // This is a programming error and will result in broken encryption + Q_ASSERT(*raw != *result); + if (*raw == *result) { + qWarning("Argon2Kdf: Input and output buffers must not be the same."); + return false; + } + result.clear(); result.resize(32); // Time Cost, Mem Cost, Threads/Lanes, Password, length, Salt, length, out, length From 67b550bb6e99195bd29c66b1f3cf83660f40bcac Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 12 Jul 2025 12:47:46 -0400 Subject: [PATCH 4/5] Address PR comments --- src/gui/ApplicationSettingsWidget.cpp | 3 ++ src/gui/ApplicationSettingsWidgetSecurity.ui | 5 +-- src/gui/DatabaseOpenWidget.ui | 23 ++++++++-- src/quickunlock/PinUnlock.cpp | 47 ++++++++++++-------- src/quickunlock/PinUnlock.h | 6 +-- src/quickunlock/Polkit.cpp | 5 --- src/quickunlock/Polkit.h | 2 - src/quickunlock/QuickUnlockInterface.cpp | 8 ---- src/quickunlock/QuickUnlockInterface.h | 3 -- src/quickunlock/TouchID.h | 4 -- src/quickunlock/TouchID.mm | 44 +++++++++++++----- src/quickunlock/WindowsHello.cpp | 7 +-- src/quickunlock/WindowsHello.h | 2 - 13 files changed, 86 insertions(+), 73 deletions(-) diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index aa3fa2471..3f1f91cad 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -353,6 +353,9 @@ void ApplicationSettingsWidget::loadSettings() #ifdef Q_OS_LINUX // Remembering quick unlock is not supported on Linux m_secUi->quickUnlockRememberCheckBox->setVisible(false); +#else + // Only show this option if Touch ID or Windows Hello are available for use + m_secUi->quickUnlockRememberCheckBox->setVisible(getQuickUnlock()->isNativeAvailable()); #endif for (const ExtraPage& page : asConst(m_extraPages)) { diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui index f708995a5..56a6f7343 100644 --- a/src/gui/ApplicationSettingsWidgetSecurity.ui +++ b/src/gui/ApplicationSettingsWidgetSecurity.ui @@ -174,11 +174,8 @@ - - Quick unlock can only be remembered when using Touch ID or Windows Hello - - Remember quick unlock after database is closed (Touch ID / Windows Hello only) + Remember quick unlock after database is closed diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui index eef43dd27..e479bcac3 100644 --- a/src/gui/DatabaseOpenWidget.ui +++ b/src/gui/DatabaseOpenWidget.ui @@ -484,7 +484,7 @@ Qt::RightToLeft - Enable Quick Unlock + Quick Unlock true @@ -609,7 +609,7 @@ 20 - 8 + 4 @@ -621,6 +621,12 @@ + + + 0 + 20 + + Reset @@ -636,7 +642,7 @@ - 6 + 4 20 @@ -644,6 +650,12 @@ + + + 0 + 20 + + Close Database @@ -742,7 +754,6 @@ - quickUnlockButton editPassword keyFileLineEdit buttonBrowseFile @@ -750,7 +761,11 @@ hardwareKeyCombo refreshHardwareKeys addKeyFileLinkLabel + enableQuickUnlockCheckBox buttonBox + quickUnlockButton + resetQuickUnlockButton + closeQuickUnlockButton diff --git a/src/quickunlock/PinUnlock.cpp b/src/quickunlock/PinUnlock.cpp index 54d7ef1ee..bcf48defb 100644 --- a/src/quickunlock/PinUnlock.cpp +++ b/src/quickunlock/PinUnlock.cpp @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2023 KeePassXC Team +/* + * Copyright (C) 2025 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 @@ -20,13 +20,17 @@ #include "crypto/CryptoHash.h" #include "crypto/Random.h" #include "crypto/SymmetricCipher.h" +#include "crypto/kdf/Argon2Kdf.h" #include #include -#define MIN_PIN_LENGTH 4 -#define MAX_PIN_LENGTH 8 -#define MAX_PIN_ATTEMPTS 3 +namespace +{ + constexpr int MIN_PIN_LENGTH = 6; + constexpr int MAX_PIN_LENGTH = 10; + constexpr int MAX_PIN_ATTEMPTS = 3; +} // namespace bool PinUnlock::isAvailable() const { @@ -47,7 +51,7 @@ bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data) pin = QInputDialog::getText( nullptr, QObject::tr("Quick Unlock Pin Entry"), - QObject::tr("Enter a %1 to %2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH), + QObject::tr("Enter a %1–%2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH), QLineEdit::Password, {}, &ok); @@ -63,15 +67,20 @@ bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data) } } - // Hash the pin and use it as the key for the encryption + // Hash the pin then run it through Argon2 to derive the encryption key + QByteArray key(32, '\0'); + Argon2Kdf kdf(Argon2Kdf::Type::Argon2id); CryptoHash hash(CryptoHash::Sha256); hash.addData(pin.toLatin1()); - auto key = hash.result(); + if (!kdf.transform(hash.result(), key)) { + m_error = QObject::tr("Failed to derive key using Argon2"); + return false; + } // Generate a random IV - auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); + const auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); - // Encrypt the data using AES-256-CBC + // Encrypt the data using AES-256-GCM SymmetricCipher cipher; if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) { m_error = QObject::tr("Failed to init KeePassXC crypto."); @@ -117,13 +126,18 @@ bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data) return false; } - // Hash the pin and use it as the key for the encryption + // Hash the pin then run it through Argon2 to derive the encryption key + QByteArray key(32, '\0'); + Argon2Kdf kdf(Argon2Kdf::Type::Argon2id); CryptoHash hash(CryptoHash::Sha256); hash.addData(pin.toLatin1()); - auto key = hash.result(); + if (!kdf.transform(hash.result(), key)) { + m_error = QObject::tr("Failed to derive key using Argon2"); + return false; + } // Read the previously used challenge and encrypted data - auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); + const auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); const auto& keydata = pairData.second; auto challenge = keydata.left(ivSize); auto encrypted = keydata.mid(ivSize); @@ -145,7 +159,7 @@ bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data) } data.clear(); - m_error = QObject::tr("Maximum pin attempts have been reached."); + m_error = QObject::tr("Too many pin attempts."); reset(dbUuid); return false; } @@ -155,11 +169,6 @@ bool PinUnlock::hasKey(const QUuid& dbUuid) const return m_encryptedKeys.contains(dbUuid); } -bool PinUnlock::canRemember() const -{ - return false; -} - void PinUnlock::reset(const QUuid& dbUuid) { m_encryptedKeys.remove(dbUuid); diff --git a/src/quickunlock/PinUnlock.h b/src/quickunlock/PinUnlock.h index c285ad8e9..acae32eeb 100644 --- a/src/quickunlock/PinUnlock.h +++ b/src/quickunlock/PinUnlock.h @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2023 KeePassXC Team +/* + * Copyright (C) 2025 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 @@ -34,8 +34,6 @@ public: bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool hasKey(const QUuid& dbUuid) const override; - bool canRemember() const override; - void reset(const QUuid& dbUuid) override; void reset() override; diff --git a/src/quickunlock/Polkit.cpp b/src/quickunlock/Polkit.cpp index 740ab12a8..c9fa5f75c 100644 --- a/src/quickunlock/Polkit.cpp +++ b/src/quickunlock/Polkit.cpp @@ -237,11 +237,6 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) return false; } -bool Polkit::canRemember() const -{ - return false; -} - bool Polkit::hasKey(const QUuid& dbUuid) const { if (!m_encryptedMasterKeys.contains(dbUuid)) { diff --git a/src/quickunlock/Polkit.h b/src/quickunlock/Polkit.h index 2dd8bbf6c..7dfc2db7b 100644 --- a/src/quickunlock/Polkit.h +++ b/src/quickunlock/Polkit.h @@ -36,8 +36,6 @@ public: bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool hasKey(const QUuid& dbUuid) const override; - bool canRemember() const override; - void reset(const QUuid& dbUuid) override; void reset() override; diff --git a/src/quickunlock/QuickUnlockInterface.cpp b/src/quickunlock/QuickUnlockInterface.cpp index 2e55dd41f..bd365ed28 100644 --- a/src/quickunlock/QuickUnlockInterface.cpp +++ b/src/quickunlock/QuickUnlockInterface.cpp @@ -68,11 +68,3 @@ bool QuickUnlockManager::isNativeAvailable() const { return m_nativeInterface && m_nativeInterface->isAvailable(); } - -bool QuickUnlockManager::isRememberAvailable() const -{ - if (isNativeAvailable()) { - return m_nativeInterface->canRemember(); - } - return m_fallbackInterface->canRemember(); -} diff --git a/src/quickunlock/QuickUnlockInterface.h b/src/quickunlock/QuickUnlockInterface.h index df55c01a8..6a999ac2a 100644 --- a/src/quickunlock/QuickUnlockInterface.h +++ b/src/quickunlock/QuickUnlockInterface.h @@ -36,8 +36,6 @@ public: virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0; virtual bool hasKey(const QUuid& dbUuid) const = 0; - virtual bool canRemember() const = 0; - virtual void reset(const QUuid& dbUuid) = 0; virtual void reset() = 0; }; @@ -52,7 +50,6 @@ public: QSharedPointer interface() const; bool isNativeAvailable() const; - bool isRememberAvailable() const; private: QSharedPointer m_nativeInterface; diff --git a/src/quickunlock/TouchID.h b/src/quickunlock/TouchID.h index ef4f5ebf3..ce73e87f9 100644 --- a/src/quickunlock/TouchID.h +++ b/src/quickunlock/TouchID.h @@ -31,8 +31,6 @@ public: bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override; bool hasKey(const QUuid& dbUuid) const override; - bool canRemember() const override; - void reset(const QUuid& dbUuid = "") override; void reset() override; @@ -44,8 +42,6 @@ private: static void deleteKeyEntry(const QString& accountName); static QString databaseKeyName(const QUuid& dbUuid); - - QHash m_encryptedMasterKeys; }; #endif // KEEPASSX_TOUCHID_H diff --git a/src/quickunlock/TouchID.mm b/src/quickunlock/TouchID.mm index 60aa45cb7..d2e6a88ba 100644 --- a/src/quickunlock/TouchID.mm +++ b/src/quickunlock/TouchID.mm @@ -25,6 +25,8 @@ inline void debug(const char *message, ...) } #endif +static const auto s_touchIdKeyPrefix = QStringLiteral("KeepassXC_TouchID_Keys_"); + inline std::string StatusToErrorMessage(OSStatus status) { CFStringRef text = SecCopyErrorMessageString(status, NULL); @@ -73,8 +75,7 @@ void TouchID::deleteKeyEntry(const QString& accountName) QString TouchID::databaseKeyName(const QUuid& dbUuid) { - static const QString keyPrefix = "KeepassXC_TouchID_Keys_"; - return keyPrefix + dbUuid.toString(); + return s_touchIdKeyPrefix + dbUuid.toString(); } QString TouchID::errorString() const @@ -85,8 +86,35 @@ QString TouchID::errorString() const void TouchID::reset() { - // TODO: Clear all credentials associated with KeePassXC - m_encryptedMasterKeys.clear(); + // Query for all generic password items + CFMutableDictionaryRef query = makeDictionary(); + CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue); + CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll); + + CFTypeRef result = nullptr; + OSStatus status = SecItemCopyMatching(query, &result); + if (status != errSecSuccess || !result) { + LogStatusError("TouchID::deleteAllKeyEntriesWithPrefix - Error querying keychain", status); + CFRelease(query); + return; + } + + NSArray* items = (__bridge NSArray*)result; + for (NSDictionary* item in items) { + NSString* account = item[(id)kSecAttrAccount]; + if (account && [account hasPrefix:s_touchIdKeyPrefix.toNSString()]) { + // Build a query to delete this item + CFMutableDictionaryRef delQuery = makeDictionary(); + CFDictionarySetValue(delQuery, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(delQuery, kSecAttrAccount, (__bridge CFStringRef)account); + OSStatus delStatus = SecItemDelete(delQuery); + LogStatusError("TouchID::deleteAllKeyEntriesWithPrefix - Error deleting item", delStatus); + CFRelease(delQuery); + } + } + CFRelease(result); + CFRelease(query); } /** @@ -184,9 +212,6 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key, const bool igno return false; } - // memorize which database the stored key is for - // TODO: Do we need to store the db uuid's to do a full reset later? - //m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); debug("TouchID::setKey - Success!"); return true; } @@ -363,11 +388,6 @@ bool TouchID::isAvailable() const return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible(); } -bool TouchID::canRemember() const -{ - return true; -} - /** * Resets the inner state either for all or for the given database */ diff --git a/src/quickunlock/WindowsHello.cpp b/src/quickunlock/WindowsHello.cpp index b1d7c94b2..7290ac73c 100644 --- a/src/quickunlock/WindowsHello.cpp +++ b/src/quickunlock/WindowsHello.cpp @@ -112,7 +112,7 @@ namespace { try { auto vault = PasswordVault(); - vault.Remove({s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString()), L"blah"}); + vault.Remove({s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString()), L"nodata"}); } catch (winrt::hresult_error const& ex) { } } @@ -234,11 +234,6 @@ bool WindowsHello::hasKey(const QUuid& dbUuid) const return !loadCredential(dbUuid).isEmpty(); } -bool WindowsHello::canRemember() const -{ - return true; -} - void WindowsHello::reset() { resetCredentials(); diff --git a/src/quickunlock/WindowsHello.h b/src/quickunlock/WindowsHello.h index 67f504bb2..0f6008590 100644 --- a/src/quickunlock/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -32,8 +32,6 @@ public: bool getKey(const QUuid& dbUuid, QByteArray& key) override; bool hasKey(const QUuid& dbUuid) const override; - bool canRemember() const override; - void reset(const QUuid& dbUuid) override; void reset() override; From f15ba49fc62e984964dbdc65ce767e6e31cab587 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 19 Jul 2025 11:14:42 -0400 Subject: [PATCH 5/5] WIP: Enable centralized secret storage * Also enables pin unlock to be stored TODO: Clean up pin unlock interface with polkit --- share/translations/keepassxc_en.ts | 66 ++- src/CMakeLists.txt | 39 +- src/gui/osutils/OSUtilsBase.h | 8 + src/gui/osutils/macutils/AppKitImpl.mm | 227 ++++++++++ src/gui/osutils/macutils/MacUtils.h | 15 + src/gui/osutils/nixutils/NixUtils.cpp | 76 ++++ src/gui/osutils/nixutils/NixUtils.h | 5 + src/gui/osutils/winutils/WinUtils.cpp | 69 +++ src/gui/osutils/winutils/WinUtils.h | 5 + src/quickunlock/PinUnlock.cpp | 159 ++++--- src/quickunlock/PinUnlock.h | 11 +- src/quickunlock/Polkit.cpp | 200 +++++---- src/quickunlock/Polkit.h | 22 +- src/quickunlock/PolkitDbusTypes.cpp | 46 ++ src/quickunlock/PolkitDbusTypes.h | 47 ++- src/quickunlock/QuickUnlockInterface.h | 9 +- src/quickunlock/TouchID.cpp | 72 ++++ src/quickunlock/TouchID.h | 16 +- src/quickunlock/TouchID.mm | 397 ------------------ src/quickunlock/WindowsHello.cpp | 83 +--- src/quickunlock/WindowsHello.h | 3 - .../org.freedesktop.PolicyKit1.Authority.xml | 5 + 22 files changed, 857 insertions(+), 723 deletions(-) create mode 100644 src/quickunlock/TouchID.cpp delete mode 100644 src/quickunlock/TouchID.mm diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 345a6b7f9..60d0d8f93 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -701,16 +701,12 @@ Hide notes in the entry preview panel - - Quick unlock can only be remembered when using Touch ID or Windows Hello - - Enable database quick unlock by default - Remember quick unlock after database is closed (Touch ID / Windows Hello only) + Remember quick unlock after database is closed @@ -1739,10 +1735,6 @@ To prevent this error from appearing, you must go to "Database Settings / S Cannot use database file as key file - - authenticate to access the database - - Failed to authenticate with Quick Unlock: %1 @@ -1795,10 +1787,6 @@ Are you sure you want to continue with this file?. <a href="#" style="text-decoration: underline">I have a key file</a> - - Enable Quick Unlock - - Reset @@ -1815,6 +1803,10 @@ Are you sure you want to continue with this file?. Press ESC again to close this database + + Quick Unlock + + DatabaseSettingWidgetMetaData @@ -9154,10 +9146,6 @@ This option is deprecated, use --set-key-file instead. Failed to encrypt key data. - - Failed to get Windows Hello credential. - - Failed to decrypt key data. @@ -9324,10 +9312,6 @@ This option is deprecated, use --set-key-file instead. Quick Unlock Pin Entry - - Enter a %1 to %2 digit pin to use for quick unlock: - - Pin setup was canceled. Quick unlock has not been enabled. @@ -9344,22 +9328,6 @@ This option is deprecated, use --set-key-file instead. Pin entry was canceled. - - Maximum pin attempts have been reached. - - - - Failed to store key in Linux Keyring. Quick unlock has not been enabled. - - - - Could not locate key in Linux Keyring. - - - - Could not read key in Linux Keyring. - - No Polkit authentication agent was available. @@ -9457,6 +9425,30 @@ This option is deprecated, use --set-key-file instead. Format to use when exporting. Available choices are 'xml', 'csv' or 'html'. Defaults to 'xml'. + + Enter a %1–%2 digit pin to use for quick unlock: + + + + Failed to derive key using Argon2 + + + + Too many pin attempts. + + + + No key is stored for this database. + + + + Failed to obtain session key. + + + + Failed to retrieve Windows Hello credential. + + QtIOCompressor diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d12da9dc0..9c5f1456d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -228,40 +228,41 @@ if(APPLE) gui/osutils/macutils/ScreenLockListenerMac.cpp gui/osutils/macutils/AppKitImpl.mm gui/osutils/macutils/AppKit.h - quickunlock/TouchID.mm) - - # TODO: Remove -Wno-error once deprecation warnings have been resolved. - set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast") + quickunlock/TouchID.cpp) endif() if(UNIX AND NOT APPLE) list(APPEND gui_SOURCES gui/osutils/nixutils/ScreenLockListenerDBus.cpp gui/osutils/nixutils/NixUtils.cpp) - if("${CMAKE_SYSTEM}" MATCHES "Linux") - list(APPEND core_SOURCES - quickunlock/Polkit.cpp - quickunlock/PolkitDbusTypes.cpp) - endif() if(WITH_XC_X11) list(APPEND gui_SOURCES gui/osutils/nixutils/X11Funcs.cpp) endif() + + # Polkit is only available on Linux systems + if("${CMAKE_SYSTEM}" MATCHES "Linux") + list(APPEND gui_SOURCES + quickunlock/Polkit.cpp + quickunlock/PolkitDbusTypes.cpp) + + set_source_files_properties( + quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml + PROPERTIES + INCLUDE "quickunlock/PolkitDbusTypes.h" + ) + qt5_add_dbus_interface(gui_SOURCES + quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml + polkit_dbus + ) + endif() + + # dbus support qt5_add_dbus_adaptor(gui_SOURCES gui/org.keepassxc.KeePassXC.MainWindow.xml gui/MainWindow.h MainWindow) - set_source_files_properties( - quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml - PROPERTIES - INCLUDE "quickunlock/PolkitDbusTypes.h" - ) - qt5_add_dbus_interface(core_SOURCES - quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml - polkit_dbus - ) - find_library(KEYUTILS_LIBRARIES NAMES keyutils) if(NOT KEYUTILS_LIBRARIES) message(FATAL_ERROR "Could not find libkeyutils") diff --git a/src/gui/osutils/OSUtilsBase.h b/src/gui/osutils/OSUtilsBase.h index 11d739fde..b43278840 100644 --- a/src/gui/osutils/OSUtilsBase.h +++ b/src/gui/osutils/OSUtilsBase.h @@ -72,6 +72,14 @@ public: virtual bool canPreventScreenCapture() const = 0; virtual bool setPreventScreenCapture(QWindow* window, bool allow) const; + /** + * Platform specific secrets storage/handling + */ + virtual bool saveSecret(const QString& key, const QByteArray& secretData) const = 0; + virtual bool getSecret(const QString& key, QByteArray& secretData) const = 0; + virtual bool removeSecret(const QString& key) const = 0; + virtual bool removeAllSecrets() const = 0; + signals: void globalShortcutTriggered(const QString& name, const QString& search = {}); diff --git a/src/gui/osutils/macutils/AppKitImpl.mm b/src/gui/osutils/macutils/AppKitImpl.mm index 43b3f3723..19645dd29 100644 --- a/src/gui/osutils/macutils/AppKitImpl.mm +++ b/src/gui/osutils/macutils/AppKitImpl.mm @@ -17,14 +17,22 @@ */ #import "AppKitImpl.h" +#import "MacUtils.h" + #import #import #import #import +#import +#import +#import +#import #if __clang_major__ >= 13 && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_VERSION_12_3 #import #endif +#include "config-keepassx.h" + @implementation AppKitImpl - (id) initWithObject:(AppKit*)appkit @@ -340,3 +348,222 @@ void AppKit::configureWindowAndHelpMenus(QMainWindow* window, QMenu* helpMenu) { [static_cast(self) configureWindowAndHelpMenus:window helpMenu:helpMenu]; } + +// Common prefix for saved secrets +static const auto s_touchIdKeyPrefix = QStringLiteral("KeepassXC_Keys_"); + +// Convert macOS error codes to strings +inline std::string StatusToErrorMessage(OSStatus status) +{ + CFStringRef text = SecCopyErrorMessageString(status, NULL); + if (!text) { + return std::to_string(status); + } + + auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8); + std::string result; + if (msg) { + result = msg; + } + CFRelease(text); + return result; +} + +// Report status errors if not successful +inline void LogStatusError(const char *message, OSStatus status) +{ + if (status) { + std::string msg = StatusToErrorMessage(status); + qWarning("%s: %s", message, msg.c_str()); + } +} + +// Create an access control object to govern use of the saved secret +SecAccessControlRef createAccessControl(bool useTouchId) +{ + // We need both runtime and compile time checks here to solve the following problems: + // - Not all flags are available in all OS versions, so we have to check it at compile time + // - Requesting Biometry/TouchID/DevicePassword when no fingerprint sensor is available will result in runtime error + SecAccessControlCreateFlags accessControlFlags = 0; + + // When TouchID is not enrolled and the flag is set, the method call fails with an error. + // We still want to set this flag if TouchID is enrolled but temporarily unavailable due to closed lid + // + // Sometimes, the enrolled-check does not work, LAErrorBiometryNotAvailable is returned instead of LAErrorBiometryNotEnrolled. + // To fallback gracefully, we have to try to save the key a second time without this flag. + + if (useTouchId) { +#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY) + // This is the non-deprecated and preferred flag + accessControlFlags = kSecAccessControlBiometryCurrentSet; +#elif XC_COMPILER_SUPPORT(TOUCH_ID) + accessControlFlags = kSecAccessControlTouchIDCurrentSet; +#endif + } + + // Add support for watch authentication if available +#if XC_COMPILER_SUPPORT(WATCH_UNLOCK) + accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch; +#endif + + // Check if password fallback is possible and add that as an option +#if XC_COMPILER_SUPPORT(TOUCH_ID) + if (macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback)) { + accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode; + } +#endif + + CFErrorRef error = nullptr; + auto sacObject = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error); + + if (!sacObject || error) { + auto e = static_cast(error); + qWarning("MacUtils::saveSecret - Error creating security flags: %s", e.localizedDescription.UTF8String); + return nullptr; + } + return sacObject; +} + +bool MacUtils::saveSecret(const QString& key, const QByteArray& secretData) const +{ + const auto keyName = s_touchIdKeyPrefix + key; + + // Delete any existing entry since macOS does not allow overwrite + if (!removeSecret(key)) { + qWarning("MacUtils::saveSecret - Failed to remove existing secret for key '%s'", qPrintable(key)); + } + + // Add new entry + auto keyBase64 = secretData.toBase64(); + auto keyValueData = CFDataCreateWithBytesNoCopy( + kCFAllocatorDefault, reinterpret_cast(keyBase64.data()), + keyBase64.length(), kCFAllocatorDefault); + + auto attributes = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(attributes, kSecAttrAccount, static_cast(keyName.toNSString())); + CFDictionarySetValue(attributes, kSecValueData, keyValueData); + CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse); + CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow); + // First, attempt with TouchID enabled + CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(true)); + + auto status = SecItemAdd(attributes, nullptr); + if (status != errSecSuccess) { + qDebug("MacUtils::saveSecret - Failed to save secret with TouchID enabled"); + // Try again without TouchID enabled + CFDictionarySetValue(attributes, kSecAttrAccessControl, createAccessControl(false)); + status = SecItemAdd(attributes, nullptr); + if (status != errSecSuccess) { + qWarning("MacUtils::saveSecret - Failed to save secret to keystore"); + } + } + + CFRelease(keyValueData); + CFRelease(attributes); + + return status == errSecSuccess; +} + +bool MacUtils::getSecret(const QString& key, QByteArray& secretData) const +{ + const auto keyName = s_touchIdKeyPrefix + key; + secretData.clear(); + + auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(query, kSecAttrAccount, static_cast(keyName.toNSString())); + CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue); + + CFTypeRef dataTypeRef = nullptr; + auto status = SecItemCopyMatching(query, &dataTypeRef); + CFRelease(query); + + if (status == errSecUserCanceled) { + // user canceled the authentication, return true with empty key + return true; + } else if (status != errSecSuccess || !dataTypeRef) { + // TODO: Log failure + return false; + } + + auto valueData = static_cast(dataTypeRef); + secretData = QByteArray::fromBase64(QByteArray(reinterpret_cast(CFDataGetBytePtr(valueData)), + CFDataGetLength(valueData))); + CFRelease(dataTypeRef); + + return !secretData.isEmpty(); +} + +bool MacUtils::removeSecret(const QString& key) const +{ + const auto keyName = s_touchIdKeyPrefix + key; + auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(query, kSecAttrAccount, static_cast(keyName.toNSString())); + CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse); + // TODO: Log failure to delete? + SecItemDelete(query); + CFRelease(query); + return true; +} + +bool MacUtils::removeAllSecrets() const +{ + auto query = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue); + CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll); + + CFTypeRef result = nullptr; + auto status = SecItemCopyMatching(query, &result); + if (status == errSecSuccess && result) { + for (NSDictionary* item in static_cast(result)) { + NSString* account = item[static_cast(kSecAttrAccount)]; + if (account && [account hasPrefix:s_touchIdKeyPrefix.toNSString()]) { + auto delQuery = CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(delQuery, kSecClass, kSecClassGenericPassword); + CFDictionarySetValue(delQuery, kSecAttrAccount, static_cast(account)); + // TODO: Log failure to delete? + SecItemDelete(delQuery); + CFRelease(delQuery); + } + } + CFRelease(result); + } + CFRelease(query); + return true; +} + +bool MacUtils::isAuthPolicyAvailable(AuthPolicy policy) const +{ + LAPolicy policyCode; + switch (policy) { + case AuthPolicy::TouchId: + policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics; + break; + case AuthPolicy::Watch: + policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch; + break; + case AuthPolicy::PasswordFallback: + policyCode = LAPolicyDeviceOwnerAuthentication; + break; + default: + return false; + } + + @try { + LAContext *context = [[LAContext alloc] init]; + NSError *error = nil; + bool available = [context canEvaluatePolicy:policyCode error:&error]; + [context release]; + if (error) { + qDebug("MacUtils::isPolicyAvailable - Policy not available: %s", error.localizedDescription.UTF8String); + } + return available; + } @catch (NSException *exception) { + qWarning("MacUtils::isPolicyAvailable - Exception occurred: %s", exception.reason.UTF8String); + return false; + } +} diff --git a/src/gui/osutils/macutils/MacUtils.h b/src/gui/osutils/macutils/MacUtils.h index 403419301..ec60db4a7 100644 --- a/src/gui/osutils/macutils/MacUtils.h +++ b/src/gui/osutils/macutils/MacUtils.h @@ -68,6 +68,21 @@ public: bool canPreventScreenCapture() const override; bool setPreventScreenCapture(QWindow* window, bool prevent) const override; + // Key management API (TouchID) + bool saveSecret(const QString& key, const QByteArray& secretData) const override; + bool getSecret(const QString& key, QByteArray& secretData) const override; + bool removeSecret(const QString& key) const override; + bool removeAllSecrets() const override; + + enum class AuthPolicy + { + TouchId, + Watch, + PasswordFallback + }; + + bool isAuthPolicyAvailable(AuthPolicy policy) const; + signals: void userSwitched(); diff --git a/src/gui/osutils/nixutils/NixUtils.cpp b/src/gui/osutils/nixutils/NixUtils.cpp index 225d6a05e..c1347161e 100644 --- a/src/gui/osutils/nixutils/NixUtils.cpp +++ b/src/gui/osutils/nixutils/NixUtils.cpp @@ -30,6 +30,11 @@ #include #include #include + +extern "C" { +#include +} + #ifdef WITH_XC_X11 #include @@ -411,3 +416,74 @@ quint64 NixUtils::getProcessStartTime() const qDebug() << "nixutils: failed to find ')' in " << processStatPath; return 0; } + +namespace +{ + key_serial_t getKeyring() + { + auto keyring = keyctl_get_persistent(-1, KEY_SPEC_PROCESS_KEYRING); + if (keyring == -1) { + // Return the non-persistent keyring as a fallback + qWarning("nixutils: failed to get persistent keyring: %s", strerror(errno)); + keyring = KEY_SPEC_PROCESS_KEYRING; + } + return keyring; + } +} // namespace + +bool NixUtils::saveSecret(const QString& key, const QByteArray& secretData) const +{ + auto keyserial = + add_key("user", key.toStdString().c_str(), secretData.constData(), secretData.size(), getKeyring()); + if (keyserial < 0) { + qWarning("nixutils: failed to save secret: %s", strerror(errno)); + return false; + } + // Only allow this process to read/write this key + keyctl_setperm(keyserial, KEY_POS_ALL); + + return true; +} + +bool NixUtils::getSecret(const QString& key, QByteArray& secretData) const +{ + secretData.clear(); + + auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring()); + if (keyserial < 0) { + qWarning("nixutils: failed to find secret: %s", strerror(errno)); + return false; + } + + secretData.resize(512); + auto size = keyctl_read(keyserial, secretData.data(), secretData.size()); + if (size == -1) { + qWarning("nixutils: failed to read secret: %s", strerror(errno)); + return false; + } + + secretData.resize(size); + return true; +} + +bool NixUtils::removeSecret(const QString& key) const +{ + auto keyserial = request_key("user", key.toStdString().c_str(), nullptr, getKeyring()); + if (keyserial < 0) { + qWarning("nixutils: failed to find secret: %s", strerror(errno)); + return false; + } + + if (keyctl_unlink(keyserial, getKeyring()) < 0) { + qWarning("nixutils: failed to remove secret: %s", strerror(errno)); + return false; + } + + return true; +} + +bool NixUtils::removeAllSecrets() const +{ + // NixUtils does not support clearing all keys + return false; +} diff --git a/src/gui/osutils/nixutils/NixUtils.h b/src/gui/osutils/nixutils/NixUtils.h index 9be835ff9..bbf4713e1 100644 --- a/src/gui/osutils/nixutils/NixUtils.h +++ b/src/gui/osutils/nixutils/NixUtils.h @@ -52,6 +52,11 @@ public: quint64 getProcessStartTime() const; + bool saveSecret(const QString& key, const QByteArray& secretData) const override; + bool getSecret(const QString& key, QByteArray& secretData) const override; + bool removeSecret(const QString& key) const override; + bool removeAllSecrets() const override; + private slots: void handleColorSchemeRead(QDBusVariant value); void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value); diff --git a/src/gui/osutils/winutils/WinUtils.cpp b/src/gui/osutils/winutils/WinUtils.cpp index a15976932..6d242963c 100644 --- a/src/gui/osutils/winutils/WinUtils.cpp +++ b/src/gui/osutils/winutils/WinUtils.cpp @@ -20,11 +20,24 @@ #include #include #include +#include #include #include +#include +#include +#include #undef MessageBox +using namespace winrt; +using namespace Windows::Foundation::Collections; +using namespace Windows::Security::Credentials; + +namespace +{ + const std::wstring s_winKeyStoreName{L"keepassxc"}; +} + QPointer WinUtils::m_instance = nullptr; WinUtils* WinUtils::instance() @@ -361,3 +374,59 @@ DWORD WinUtils::qtToNativeModifiers(Qt::KeyboardModifiers modifiers) return nativeModifiers; } + +bool WinUtils::saveSecret(const QString& key, const QByteArray& secretData) const +{ + try { + auto vault = PasswordVault(); + vault.Add({s_winKeyStoreName, + winrt::hstring(key.toStdWString()), + winrt::to_hstring(secretData.toBase64().toStdString())}); + return true; + } catch (winrt::hresult_error const&) { + qWarning("WinUtils - Failed to add key to password vault"); + return false; + } +} + +bool WinUtils::getSecret(const QString& key, QByteArray& secretData) const +{ + secretData.clear(); + try { + auto vault = PasswordVault(); + auto credential = vault.Retrieve(s_winKeyStoreName, winrt::hstring(key.toStdWString())); + secretData = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password()))); + } catch (winrt::hresult_error const&) { + qWarning("WinUtils - Failed to retrieve key from password vault"); + return false; + } + return !secretData.isEmpty(); +} + +bool WinUtils::removeSecret(const QString& key) const +{ + try { + auto vault = PasswordVault(); + vault.Remove({s_winKeyStoreName, winrt::hstring(key.toStdWString()), L"nodata"}); + return true; + } catch (winrt::hresult_error const&) { + qWarning("WinUtils - Failed to clear key from password vault"); + return false; + } +} + +bool WinUtils::removeAllSecrets() const +{ + auto vault = PasswordVault(); + auto credentials = vault.FindAllByResource(s_winKeyStoreName); + bool allSuccess = true; + for (const auto& credential : credentials) { + try { + vault.Remove(credential); + } catch (winrt::hresult_error const&) { + qWarning("WinUtils - Failed to clear key from password vault"); + allSuccess = false; + } + } + return allSuccess; +} diff --git a/src/gui/osutils/winutils/WinUtils.h b/src/gui/osutils/winutils/WinUtils.h index 9278c9d60..680d19196 100644 --- a/src/gui/osutils/winutils/WinUtils.h +++ b/src/gui/osutils/winutils/WinUtils.h @@ -61,6 +61,11 @@ public: bool canPreventScreenCapture() const override; bool setPreventScreenCapture(QWindow* window, bool prevent) const override; + bool saveSecret(const QString& key, const QByteArray& secretData) const override; + bool getSecret(const QString& key, QByteArray& secretData) const override; + bool removeSecret(const QString& key) const override; + bool removeAllSecrets() const override; + protected: explicit WinUtils(QObject* parent = nullptr); ~WinUtils() override = default; diff --git a/src/quickunlock/PinUnlock.cpp b/src/quickunlock/PinUnlock.cpp index bcf48defb..d4ca6ca8d 100644 --- a/src/quickunlock/PinUnlock.cpp +++ b/src/quickunlock/PinUnlock.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2025 KeePassXC Team * * This program is free software: you can redistribute it and/or modify @@ -21,62 +21,86 @@ #include "crypto/Random.h" #include "crypto/SymmetricCipher.h" #include "crypto/kdf/Argon2Kdf.h" +#include "gui/osutils/OSUtils.h" #include #include -namespace -{ - constexpr int MIN_PIN_LENGTH = 6; - constexpr int MAX_PIN_LENGTH = 10; - constexpr int MAX_PIN_ATTEMPTS = 3; -} // namespace +const int PinUnlock::MIN_PIN_LENGTH = 6; +const int PinUnlock::MAX_PIN_LENGTH = 10; +const int PinUnlock::MAX_PIN_ATTEMPTS = 3; bool PinUnlock::isAvailable() const { return true; } -QString PinUnlock::errorString() const -{ - return m_error; -} - -bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data) +bool PinUnlock::promptPin(int attempt, QByteArray& sessionKey) { QString pin; - QRegularExpression pinRegex("^\\d+$"); - while (true) { + + if (attempt == 0) { + // Loop until a valid pin has been entered or canceled + QRegularExpression pinRegex("^\\d+$"); + while (true) { + bool ok = false; + pin = QInputDialog::getText( + nullptr, + QObject::tr("Quick Unlock Pin Entry"), + QObject::tr("Enter a %1–%2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH), + QLineEdit::Password, + {}, + &ok); + + if (!ok) { + m_error = QObject::tr("Pin setup was canceled. Quick unlock has not been enabled."); + return false; + } + + // Validate pin criteria + if (pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH && pinRegex.match(pin).hasMatch()) { + // Pin is valid, move to hashing + break; + } + } + } else { bool ok = false; pin = QInputDialog::getText( nullptr, QObject::tr("Quick Unlock Pin Entry"), - QObject::tr("Enter a %1–%2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH), + QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(attempt).arg(MAX_PIN_ATTEMPTS), QLineEdit::Password, {}, &ok); if (!ok) { - m_error = QObject::tr("Pin setup was canceled. Quick unlock has not been enabled."); + // User canceled the pin entry dialog, record pin attempts + m_error = QObject::tr("Pin entry was canceled."); return false; } - - // Validate pin criteria - if (pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH && pinRegex.match(pin).hasMatch()) { - break; - } } // Hash the pin then run it through Argon2 to derive the encryption key - QByteArray key(32, '\0'); + sessionKey.fill('\0', 32); Argon2Kdf kdf(Argon2Kdf::Type::Argon2id); CryptoHash hash(CryptoHash::Sha256); hash.addData(pin.toLatin1()); - if (!kdf.transform(hash.result(), key)) { + if (!kdf.transform(hash.result(), sessionKey)) { m_error = QObject::tr("Failed to derive key using Argon2"); return false; } + return true; +} + +bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data) +{ + QByteArray key; + if (!promptPin(0, key)) { + // Pin entry was canceled or failed, error set by promptPin + return false; + } + // Generate a random IV const auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); @@ -92,68 +116,53 @@ bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data) return false; } - // Prepend the IV to the encrypted data - encrypted.prepend(iv); - // Store the encrypted data and pin attempts - m_encryptedKeys.insert(dbUuid, qMakePair(1, encrypted)); - + // Store the encrypted data + saveKey(dbUuid, encrypted.prepend(iv)); return true; } bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data) { data.clear(); - if (!hasKey(dbUuid)) { - m_error = QObject::tr("Failed to get credentials for quick unlock."); - return false; - } - - const auto& pairData = m_encryptedKeys.value(dbUuid); - - // Restrict pin attempts per database - for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) { - bool ok = false; - auto pin = QInputDialog::getText( - nullptr, - QObject::tr("Quick Unlock Pin Entry"), - QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(pinAttempts).arg(MAX_PIN_ATTEMPTS), - QLineEdit::Password, - {}, - &ok); - - if (!ok) { - m_error = QObject::tr("Pin entry was canceled."); + bool hasSecret = m_encryptedKeys.contains(dbUuid); + if (!hasSecret) { + // Check if the OS has a secret stored for this database UUID + QByteArray tmp; + if (osUtils->getSecret(dbUuid.toString(), tmp)) { + // Cache the secret in memory + m_encryptedKeys.insert(dbUuid, qMakePair(1, tmp)); + } else { + m_error = QObject::tr("Failed to get credentials for quick unlock."); return false; } + } - // Hash the pin then run it through Argon2 to derive the encryption key - QByteArray key(32, '\0'); - Argon2Kdf kdf(Argon2Kdf::Type::Argon2id); - CryptoHash hash(CryptoHash::Sha256); - hash.addData(pin.toLatin1()); - if (!kdf.transform(hash.result(), key)) { - m_error = QObject::tr("Failed to derive key using Argon2"); + // Restrict pin attempts per database + const auto& pairData = m_encryptedKeys.value(dbUuid); + for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) { + QByteArray key; + if (!promptPin(pinAttempts, key)) { + // Pin entry was canceled or failed, error set by promptPin + m_encryptedKeys.insert(dbUuid, qMakePair(pinAttempts, pairData.second)); return false; } // Read the previously used challenge and encrypted data - const auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); - const auto& keydata = pairData.second; - auto challenge = keydata.left(ivSize); - auto encrypted = keydata.mid(ivSize); + const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); + const auto& iv = pairData.second.left(ivSize); // Decrypt the data using the generated key and IV from above SymmetricCipher cipher; - if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) { + if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) { m_error = QObject::tr("Failed to init KeePassXC crypto."); return false; } - // Store the decrypted data into the passed parameter - data = encrypted; + // Attempt to decrypt the key data + data = pairData.second.mid(ivSize); if (cipher.finish(data)) { - // Reset the pin attempts - m_encryptedKeys.insert(dbUuid, qMakePair(1, keydata)); + // Decryption succeeded, reset the pin attempts + m_encryptedKeys.insert(dbUuid, qMakePair(1, pairData.second)); return true; } } @@ -164,17 +173,35 @@ bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data) return false; } +void PinUnlock::saveKey(const QUuid& dbUuid, const QByteArray& data) +{ + // Save the key to the OS secret store + if (!osUtils->saveSecret(dbUuid.toString(), data)) { + qWarning("PinUnlock - Failed to save quick unlock credentials."); + } + // Store the encrypted key in memory + m_encryptedKeys.insert(dbUuid, qMakePair(1, data)); +} + bool PinUnlock::hasKey(const QUuid& dbUuid) const { - return m_encryptedKeys.contains(dbUuid); + bool hasSecret = m_encryptedKeys.contains(dbUuid); + if (!hasSecret) { + // Check if the OS has a secret stored for this database UUID + QByteArray tmp; + hasSecret = osUtils->getSecret(dbUuid.toString(), tmp); + } + return hasSecret; } void PinUnlock::reset(const QUuid& dbUuid) { m_encryptedKeys.remove(dbUuid); + osUtils->removeSecret(dbUuid.toString()); } void PinUnlock::reset() { m_encryptedKeys.clear(); + osUtils->removeAllSecrets(); } diff --git a/src/quickunlock/PinUnlock.h b/src/quickunlock/PinUnlock.h index acae32eeb..081b74405 100644 --- a/src/quickunlock/PinUnlock.h +++ b/src/quickunlock/PinUnlock.h @@ -28,7 +28,6 @@ public: PinUnlock() = default; bool isAvailable() const override; - QString errorString() const override; bool setKey(const QUuid& dbUuid, const QByteArray& key) override; bool getKey(const QUuid& dbUuid, QByteArray& key) override; @@ -37,8 +36,16 @@ public: void reset(const QUuid& dbUuid) override; void reset() override; + static const int MIN_PIN_LENGTH; + static const int MAX_PIN_LENGTH; + static const int MAX_PIN_ATTEMPTS; + +protected: + bool promptPin(int attempt, QByteArray& sessionKey); + private: - QString m_error; + void saveKey(const QUuid& dbUuid, const QByteArray& key); + QHash> m_encryptedKeys; Q_DISABLE_COPY(PinUnlock) diff --git a/src/quickunlock/Polkit.cpp b/src/quickunlock/Polkit.cpp index c9fa5f75c..d22f785b2 100644 --- a/src/quickunlock/Polkit.cpp +++ b/src/quickunlock/Polkit.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 KeePassXC Team + * Copyright (C) 2025 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 @@ -23,8 +23,8 @@ #include "gui/osutils/nixutils/NixUtils.h" #include -#include #include + #include #include @@ -35,19 +35,11 @@ extern "C" { const QString polkit_service = "org.freedesktop.PolicyKit1"; const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority"; -namespace -{ - QString getKeyName(const QUuid& dbUuid) - { - static const QString keyPrefix = "keepassxc_polkit_keys_"; - return keyPrefix + dbUuid.toString(); - } -} // namespace - Polkit::Polkit() { PolkitSubject::registerMetaType(); PolkitAuthorizationResults::registerMetaType(); + PolkitActionDescription::registerMetaType(); /* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden through an environment variable to return an alternative bus path. This bus could have an application @@ -61,18 +53,34 @@ Polkit::Polkit() m_available = bus.isConnected(); if (!m_available) { - qDebug() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)"; + qWarning() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)"; return; } m_available = bus.interface()->isServiceRegistered(polkit_service); if (!m_available) { - qDebug() << "polkit: Polkit is not registered on dbus"; + qWarning() << "polkit: Polkit is not registered on dbus"; return; } + // Initiate the Polkit dbus interface m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus)); + + // Reset available state and check Polkit registered actions for KeePassXC + m_available = false; + auto kpxcAction = QStringLiteral("org.keepassxc.KeePassXC.unlockDatabase"); + auto actions = m_polkit->EnumerateActions(""); + for (const auto& action : actions.value()) { + if (action.actionId == kpxcAction) { + m_available = true; + break; + } + } + + if (!m_available) { + qWarning() << "polkit: KeePassXC Polkit action is not installed"; + } } Polkit::~Polkit() @@ -81,7 +89,8 @@ Polkit::~Polkit() void Polkit::reset(const QUuid& dbUuid) { - m_encryptedMasterKeys.remove(dbUuid); + m_sessionKeys.remove(dbUuid); + nixUtils()->removeSecret(dbUuid.toString()); } bool Polkit::isAvailable() const @@ -89,67 +98,100 @@ bool Polkit::isAvailable() const return m_available; } -QString Polkit::errorString() const -{ - return m_error; -} - void Polkit::reset() { - m_encryptedMasterKeys.clear(); + m_sessionKeys.clear(); + nixUtils()->removeAllSecrets(); } -bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key) +bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& data) { reset(dbUuid); - // Generate a random iv/key pair to encrypt the master password with - QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); - QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); - QByteArray keychainKeyValue = randomKey + randomIV; + // Prompt for a pin to use as session key + QByteArray key; + if (!promptPin(0, key)) { + return false; + } + + auto iv = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); SymmetricCipher aes256Encrypt; - if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { + if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) { m_error = QObject::tr("Failed to init KeePassXC crypto."); return false; } - // Encrypt the master password - QByteArray encryptedMasterKey = key; - if (!aes256Encrypt.finish(encryptedMasterKey)) { + // Encrypt the database key + QByteArray encrypted = data; + if (!aes256Encrypt.finish(encrypted)) { m_error = QObject::tr("Failed to encrypt key data."); - qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString(); return false; } - // Add the iv/key pair into the linux keyring - key_serial_t key_serial = add_key("user", - getKeyName(dbUuid).toStdString().c_str(), - keychainKeyValue.constData(), - keychainKeyValue.size(), - KEY_SPEC_PROCESS_KEYRING); - if (key_serial < 0) { - m_error = QObject::tr("Failed to store key in Linux Keyring. Quick unlock has not been enabled."); - qDebug() << "polkit keyring failed to store: " << errno; - return false; - } + // Store the session key and save the encrypted master key to the keyring + m_sessionKeys.insert(dbUuid, key); + nixUtils()->saveSecret(dbUuid.toString(), encrypted.prepend(iv)); - // Scrub the keys from ram - Botan::secure_scrub_memory(randomKey.data(), randomKey.size()); - Botan::secure_scrub_memory(randomIV.data(), randomIV.size()); - Botan::secure_scrub_memory(keychainKeyValue.data(), keychainKeyValue.size()); - - // Store encrypted master password and return - m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); return true; } -bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) +bool Polkit::getKey(const QUuid& dbUuid, QByteArray& data) { - if (!m_polkit || !hasKey(dbUuid)) { + if (!m_available || !hasKey(dbUuid)) { + m_error = QObject::tr("No key is stored for this database."); return false; } + QByteArray key; + for (int pinAttempts = 1; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) { + if (!m_sessionKeys.contains(dbUuid)) { + // Request pin to obtain a session key + if (!promptPin(pinAttempts, key)) { + m_error = QObject::tr("Failed to obtain session key."); + return false; + } + } else { + // We already have the session key, prompt using polkit to authorize use + if (!promptPolkit()) { + // Error set in promptPolkit call + return false; + } + key = m_sessionKeys.value(dbUuid); + } + + // Retrieve the encrypted master key from the OS secret store + QByteArray encData; + if (!nixUtils()->getSecret(dbUuid.toString(), encData)) { + m_error = QObject::tr("Failed to get credentials for quick unlock."); + return false; + } + + const auto& ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); + const auto& iv = encData.left(ivSize); + + // Decrypt the data using the generated key and IV from above + SymmetricCipher cipher; + if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) { + m_error = QObject::tr("Failed to init KeePassXC crypto."); + return false; + } + + // Attempt to decrypt the key data + data = encData.mid(ivSize); + if (cipher.finish(data)) { + // Decryption succeeded, store the session key used + m_sessionKeys.insert(dbUuid, key); + return true; + } + } + + m_error = QObject::tr("Too many pin attempts."); + return false; +} + +bool Polkit::promptPolkit() +{ PolkitSubject subject; subject.kind = "unix-process"; subject.details.insert("pid", static_cast(QCoreApplication::applicationPid())); @@ -170,60 +212,11 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) if (result.isError()) { auto msg = result.error().message(); m_error = QObject::tr("Polkit returned an error: %1").arg(msg); - qDebug() << "polkit returned an error: " << msg; return false; } PolkitAuthorizationResults authResult = result.value(); if (authResult.is_authorized) { - QByteArray encryptedMasterKey = m_encryptedMasterKeys.value(dbUuid); - key_serial_t keySerial = - find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING); - - if (keySerial == -1) { - m_error = QObject::tr("Could not locate key in Linux Keyring."); - qDebug() << "polkit keyring failed to find: " << errno; - return false; - } - - void* keychainBuffer; - long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer); - - if (keychainDataSize == -1) { - m_error = QObject::tr("Could not read key in Linux Keyring."); - qDebug() << "polkit keyring failed to read: " << errno; - return false; - } - - QByteArray keychainBytes(static_cast(keychainBuffer), keychainDataSize); - - Botan::secure_scrub_memory(keychainBuffer, keychainDataSize); - free(keychainBuffer); - - QByteArray keychainKey = keychainBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); - QByteArray keychainIv = keychainBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM)); - - SymmetricCipher aes256Decrypt; - if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) { - m_error = QObject::tr("Failed to init KeePassXC crypto."); - qDebug() << "polkit aes init failed"; - return false; - } - - key = encryptedMasterKey; - if (!aes256Decrypt.finish(key)) { - key.clear(); - m_error = QObject::tr("Failed to decrypt key data."); - qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString(); - return false; - } - - // Scrub the keys from ram - Botan::secure_scrub_memory(keychainKey.data(), keychainKey.size()); - Botan::secure_scrub_memory(keychainIv.data(), keychainIv.size()); - Botan::secure_scrub_memory(keychainBytes.data(), keychainBytes.size()); - Botan::secure_scrub_memory(encryptedMasterKey.data(), encryptedMasterKey.size()); - return true; } @@ -233,15 +226,12 @@ bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key) } else { m_error = QObject::tr("Polkit authorization failed."); } - return false; } bool Polkit::hasKey(const QUuid& dbUuid) const { - if (!m_encryptedMasterKeys.contains(dbUuid)) { - return false; - } - - return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1; + // Check if the OS has a secret stored for this database UUID + QByteArray tmp; + return nixUtils()->getSecret(dbUuid.toString(), tmp); } diff --git a/src/quickunlock/Polkit.h b/src/quickunlock/Polkit.h index 7dfc2db7b..f3033e9c4 100644 --- a/src/quickunlock/Polkit.h +++ b/src/quickunlock/Polkit.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 KeePassXC Team + * Copyright (C) 2025 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 @@ -15,36 +15,34 @@ * along with this program. If not, see . */ -#ifndef KEEPASSX_POLKIT_H -#define KEEPASSX_POLKIT_H +#pragma once -#include "QuickUnlockInterface.h" +#include "PinUnlock.h" #include "polkit_dbus.h" + #include #include -class Polkit : public QuickUnlockInterface +class Polkit : public PinUnlock { public: Polkit(); ~Polkit() override; bool isAvailable() const override; - QString errorString() const override; - bool setKey(const QUuid& dbUuid, const QByteArray& key) override; - bool getKey(const QUuid& dbUuid, QByteArray& key) override; + bool setKey(const QUuid& dbUuid, const QByteArray& data) override; + bool getKey(const QUuid& dbUuid, QByteArray& data) override; bool hasKey(const QUuid& dbUuid) const override; void reset(const QUuid& dbUuid) override; void reset() override; private: + bool promptPolkit(); + bool m_available; - QString m_error; - QHash m_encryptedMasterKeys; + QHash m_sessionKeys; QScopedPointer m_polkit; }; - -#endif // KEEPASSX_POLKIT_H diff --git a/src/quickunlock/PolkitDbusTypes.cpp b/src/quickunlock/PolkitDbusTypes.cpp index a4305dc44..618b3c011 100644 --- a/src/quickunlock/PolkitDbusTypes.cpp +++ b/src/quickunlock/PolkitDbusTypes.cpp @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2025 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 "PolkitDbusTypes.h" void PolkitSubject::registerMetaType() @@ -43,3 +60,32 @@ const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizati argument.endStructure(); return argument; } + +void PolkitActionDescription::registerMetaType() +{ + qRegisterMetaType("PolkitActionDescription"); + qDBusRegisterMetaType(); + + qRegisterMetaType("PolkitActionDescriptionList"); + qDBusRegisterMetaType(); +} + +QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action) +{ + argument.beginStructure(); + argument << action.actionId << action.description << action.message << action.vendorName << action.vendorUrl + << action.iconName << action.implicitAny << action.implicitInactive << action.implicitActive + << action.annotations; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action) +{ + argument.beginStructure(); + argument >> action.actionId >> action.description >> action.message >> action.vendorName >> action.vendorUrl + >> action.iconName >> action.implicitAny >> action.implicitInactive >> action.implicitActive + >> action.annotations; + argument.endStructure(); + return argument; +} diff --git a/src/quickunlock/PolkitDbusTypes.h b/src/quickunlock/PolkitDbusTypes.h index 83eb23889..8fb6daa31 100644 --- a/src/quickunlock/PolkitDbusTypes.h +++ b/src/quickunlock/PolkitDbusTypes.h @@ -1,5 +1,21 @@ -#ifndef KEEPASSX_POLKITDBUSTYPES_H -#define KEEPASSX_POLKITDBUSTYPES_H +/* + * Copyright (C) 2025 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 . + */ + +#pragma once #include @@ -30,7 +46,30 @@ public: friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject); }; +class PolkitActionDescription +{ +public: + QString actionId; + QString description; + QString message; + QString vendorName; + QString vendorUrl; + QString iconName; + uint implicitAny; + uint implicitInactive; + uint implicitActive; + QMap annotations; + + static void registerMetaType(); + + friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitActionDescription& action); + + friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitActionDescription& action); +}; + +typedef QList PolkitActionDescriptionList; + Q_DECLARE_METATYPE(PolkitSubject); Q_DECLARE_METATYPE(PolkitAuthorizationResults); - -#endif // KEEPASSX_POLKITDBUSTYPES_H +Q_DECLARE_METATYPE(PolkitActionDescription); +Q_DECLARE_METATYPE(PolkitActionDescriptionList); diff --git a/src/quickunlock/QuickUnlockInterface.h b/src/quickunlock/QuickUnlockInterface.h index 6a999ac2a..8ca09d47e 100644 --- a/src/quickunlock/QuickUnlockInterface.h +++ b/src/quickunlock/QuickUnlockInterface.h @@ -30,7 +30,6 @@ public: virtual ~QuickUnlockInterface() = default; virtual bool isAvailable() const = 0; - virtual QString errorString() const = 0; virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0; virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0; @@ -38,6 +37,14 @@ public: virtual void reset(const QUuid& dbUuid) = 0; virtual void reset() = 0; + + virtual QString errorString() const + { + return m_error; + } + +protected: + QString m_error; }; class QuickUnlockManager final diff --git a/src/quickunlock/TouchID.cpp b/src/quickunlock/TouchID.cpp new file mode 100644 index 000000000..a36362b63 --- /dev/null +++ b/src/quickunlock/TouchID.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 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 "quickunlock/TouchID.h" +#include "gui/osutils/OSUtils.h" + +/** + * Store the serialized database key into the macOS key store. The OS handles encrypt/decrypt operations. + * https://developer.apple.com/documentation/security/keychain_services/keychain_items + */ +bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key) +{ + if (key.isEmpty()) { + qWarning("TouchID::setKey - provided key is empty"); + return false; + } + + return osUtils->saveSecret(dbUuid.toString(), key); +} + +/** + * Retrieve serialized key data from the macOS Keychain after successful authentication + * with TouchID or Watch interface. + */ +bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key) +{ + key.clear(); + + if (!hasKey(dbUuid)) { + qWarning("TouchID::getKey - No stored key found"); + return false; + } + + return osUtils->getSecret(dbUuid.toString(), key); +} + +bool TouchID::hasKey(const QUuid& dbUuid) const +{ + QByteArray tmp; + return osUtils->getSecret(dbUuid.toString(), tmp); +} + +bool TouchID::isAvailable() const +{ + return macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::TouchId) + || macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::Watch) + || macUtils()->isAuthPolicyAvailable(MacUtils::AuthPolicy::PasswordFallback); +} + +void TouchID::reset(const QUuid& dbUuid) +{ + osUtils->removeSecret(dbUuid.toString()); +} + +void TouchID::reset() +{ + osUtils->removeAllSecrets(); +} diff --git a/src/quickunlock/TouchID.h b/src/quickunlock/TouchID.h index ce73e87f9..886710cbf 100644 --- a/src/quickunlock/TouchID.h +++ b/src/quickunlock/TouchID.h @@ -15,17 +15,14 @@ * along with this program. If not, see . */ -#ifndef KEEPASSX_TOUCHID_H -#define KEEPASSX_TOUCHID_H +#pragma once #include "QuickUnlockInterface.h" -#include class TouchID : public QuickUnlockInterface { public: bool isAvailable() const override; - QString errorString() const override; bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override; bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override; @@ -33,15 +30,4 @@ public: void reset(const QUuid& dbUuid = "") override; void reset() override; - -private: - static bool isWatchAvailable(); - static bool isTouchIdAvailable(); - static bool isPasswordFallbackPossible(); - bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID); - - static void deleteKeyEntry(const QString& accountName); - static QString databaseKeyName(const QUuid& dbUuid); }; - -#endif // KEEPASSX_TOUCHID_H diff --git a/src/quickunlock/TouchID.mm b/src/quickunlock/TouchID.mm deleted file mode 100644 index d2e6a88ba..000000000 --- a/src/quickunlock/TouchID.mm +++ /dev/null @@ -1,397 +0,0 @@ -#include "quickunlock/TouchID.h" - -#include "crypto/Random.h" -#include "crypto/SymmetricCipher.h" -#include "crypto/CryptoHash.h" -#include "config-keepassx.h" - -#include - -#include -#include -#include -#include - -#include -#include - -#define TOUCH_ID_ENABLE_DEBUG_LOGS() 0 -#if TOUCH_ID_ENABLE_DEBUG_LOGS() -#define debug(...) qWarning(__VA_ARGS__) -#else -inline void debug(const char *message, ...) -{ - Q_UNUSED(message); -} -#endif - -static const auto s_touchIdKeyPrefix = QStringLiteral("KeepassXC_TouchID_Keys_"); - -inline std::string StatusToErrorMessage(OSStatus status) -{ - CFStringRef text = SecCopyErrorMessageString(status, NULL); - if (!text) { - return std::to_string(status); - } - - auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8); - std::string result; - if (msg) { - result = msg; - } - CFRelease(text); - return result; -} - -inline void LogStatusError(const char *message, OSStatus status) -{ - if (!status) { - return; - } - - std::string msg = StatusToErrorMessage(status); - debug("%s: %s", message, msg.c_str()); -} - -inline CFMutableDictionaryRef makeDictionary() { - return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); -} - -//! Try to delete an existing keychain entry -void TouchID::deleteKeyEntry(const QString& accountName) -{ - NSString* nsAccountName = accountName.toNSString(); // The NSString is released by Qt - - // try to delete an existing entry - CFMutableDictionaryRef query = makeDictionary(); - CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); - CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) nsAccountName); - CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse); - - // get data from the KeyChain - OSStatus status = SecItemDelete(query); - LogStatusError("TouchID::deleteKeyEntry - Error deleting existing entry", status); -} - -QString TouchID::databaseKeyName(const QUuid& dbUuid) -{ - return s_touchIdKeyPrefix + dbUuid.toString(); -} - -QString TouchID::errorString() const -{ - // TODO - return ""; -} - -void TouchID::reset() -{ - // Query for all generic password items - CFMutableDictionaryRef query = makeDictionary(); - CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); - CFDictionarySetValue(query, kSecReturnAttributes, kCFBooleanTrue); - CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll); - - CFTypeRef result = nullptr; - OSStatus status = SecItemCopyMatching(query, &result); - if (status != errSecSuccess || !result) { - LogStatusError("TouchID::deleteAllKeyEntriesWithPrefix - Error querying keychain", status); - CFRelease(query); - return; - } - - NSArray* items = (__bridge NSArray*)result; - for (NSDictionary* item in items) { - NSString* account = item[(id)kSecAttrAccount]; - if (account && [account hasPrefix:s_touchIdKeyPrefix.toNSString()]) { - // Build a query to delete this item - CFMutableDictionaryRef delQuery = makeDictionary(); - CFDictionarySetValue(delQuery, kSecClass, kSecClassGenericPassword); - CFDictionarySetValue(delQuery, kSecAttrAccount, (__bridge CFStringRef)account); - OSStatus delStatus = SecItemDelete(delQuery); - LogStatusError("TouchID::deleteAllKeyEntriesWithPrefix - Error deleting item", delStatus); - CFRelease(delQuery); - } - } - CFRelease(result); - CFRelease(query); -} - -/** - * Store the serialized database key into the macOS key store. The OS handles encrypt/decrypt operations. - * https://developer.apple.com/documentation/security/keychain_services/keychain_items - */ -bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key, const bool ignoreTouchID) -{ - if (key.isEmpty()) { - debug("TouchID::setKey - illegal arguments"); - return false; - } - - const auto keyName = databaseKeyName(dbUuid); - // Try to delete the existing key entry - deleteKeyEntry(keyName); - - // prepare adding secure entry to the macOS KeyChain - CFErrorRef error = NULL; - - // We need both runtime and compile time checks here to solve the following problems: - // - Not all flags are available in all OS versions, so we have to check it at compile time - // - Requesting Biometry/TouchID/DevicePassword when to fingerprint sensor is available will result in runtime error - SecAccessControlCreateFlags accessControlFlags = 0; -#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY) - // Needs a special check to work with SecItemAdd, when TouchID is not enrolled and the flag - // is set, the method call fails with an error. But we want to still set this flag if TouchID is - // enrolled but temporarily unavailable due to closed lid - // - // At least on a Hackintosh the enrolled-check does not work, there LAErrorBiometryNotAvailable gets returned instead of - // LAErrorBiometryNotEnrolled. - // - // That's kinda unfortunate, because now you cannot know for sure if TouchID hardware is either temporarily unavailable or not present - // at all, because LAErrorBiometryNotAvailable is used for both cases. - // - // So to make quick unlock fallbacks possible on these machines you have to try to save the key a second time without this flag, if the - // first try fails with an error. - if (!ignoreTouchID) { - // Prefer the non-deprecated flag when available - accessControlFlags = kSecAccessControlBiometryCurrentSet; - } -#elif XC_COMPILER_SUPPORT(TOUCH_ID) - if (!ignoreTouchID) { - accessControlFlags = kSecAccessControlTouchIDCurrentSet; - } -#endif - -#if XC_COMPILER_SUPPORT(WATCH_UNLOCK) - accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch; -#endif - -#if XC_COMPILER_SUPPORT(TOUCH_ID) - if (isPasswordFallbackPossible()) { - accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode; - } -#endif - - SecAccessControlRef sacObject = SecAccessControlCreateWithFlags( - kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error); - - if (sacObject == NULL || error != NULL) { - auto e = (__bridge NSError*) error; - debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String); - return false; - } - - auto accountName = keyName.toNSString(); - auto keyBase64 = key.toBase64(); - - // prepare data (key) to be stored - auto keyValueData = CFDataCreateWithBytesNoCopy( - kCFAllocatorDefault, reinterpret_cast(keyBase64.data()), - keyBase64.length(), kCFAllocatorDefault); - - auto attributes = makeDictionary(); - CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword); - CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName); - CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keyValueData); - CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse); - CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow); -#ifndef QT_DEBUG - // Only use TouchID when in release build, also requires application entitlements and signing - CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject); -#endif - - // add to KeyChain - OSStatus status = SecItemAdd(attributes, NULL); - LogStatusError("TouchID::setKey - Error adding new keychain item", status); - - CFRelease(sacObject); - CFRelease(attributes); - - // Cleanse the key information from the memory - if (status != errSecSuccess) { - return false; - } - - debug("TouchID::setKey - Success!"); - return true; -} - -/** - * Generates a random AES 256bit key and uses it to encrypt the PasswordKey that - * protects the database. The encrypted PasswordKey is kept in memory while the - * AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch. - */ -bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey) -{ - if (!setKey(dbUuid,passwordKey, false)) { - debug("TouchID::setKey failed with error trying fallback method without TouchID flag"); - return setKey(dbUuid, passwordKey, true); - } else { - return true; - } -} - -/** - * Retrieve serialized key data from the macOS Keychain after successful authentication - * with TouchID or Watch interface. - */ -bool TouchID::getKey(const QUuid& dbUuid, QByteArray& key) -{ - key.clear(); - - if (!hasKey(dbUuid)) { - debug("TouchID::getKey - No stored key found"); - return false; - } - - // query the KeyChain for the AES key - CFMutableDictionaryRef query = makeDictionary(); - - const QString keyName = databaseKeyName(dbUuid); - NSString* accountName = keyName.toNSString(); // The NSString is released by Qt - NSString* touchPromptMessage = - QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database") - .toNSString(); // The NSString is released by Qt - - CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); - CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName); - CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue); - CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef) touchPromptMessage); - - // get data from the KeyChain - CFTypeRef dataTypeRef = NULL; - OSStatus status = SecItemCopyMatching(query, &dataTypeRef); - CFRelease(query); - - if (status == errSecUserCanceled) { - // user canceled the authentication, return true with empty key - debug("TouchID::getKey - User canceled authentication"); - return true; - } else if (status != errSecSuccess || dataTypeRef == NULL) { - LogStatusError("TouchID::getKey - key query error", status); - return false; - } - - // Convert value returned to serialized key - CFDataRef valueData = static_cast(dataTypeRef); - key = QByteArray::fromBase64(QByteArray(reinterpret_cast(CFDataGetBytePtr(valueData)), - CFDataGetLength(valueData))); - CFRelease(dataTypeRef); - - return true; -} - -bool TouchID::hasKey(const QUuid& dbUuid) const -{ - const QString keyName = databaseKeyName(dbUuid); - NSString* accountName = keyName.toNSString(); // The NSString is released by Qt - - CFMutableDictionaryRef query = makeDictionary(); - CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); - CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName); - CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse); - - CFTypeRef item = NULL; - OSStatus status = SecItemCopyMatching(query, &item); - CFRelease(query); - - return status == errSecSuccess; -} - -// TODO: Both functions below should probably handle the returned errors to -// provide more information on availability. E.g.: the closed laptop lid results -// in an error (because touch id is not unavailable). That error could be -// displayed to the user when we first check for availability instead of just -// hiding the checkbox. - -//! @return true if Apple Watch is available for authentication. -bool TouchID::isWatchAvailable() -{ -#if XC_COMPILER_SUPPORT(WATCH_UNLOCK) - @try { - LAContext *context = [[LAContext alloc] init]; - - LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch; - NSError *error; - - bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error]; - [context release]; - if (error) { - debug("Apple Watch is not available: %s", error.localizedDescription.UTF8String); - } - return canAuthenticate; - } @catch (NSException *) { - return false; - } -#else - return false; -#endif -} - -//! @return true if Touch ID is available for authentication. -bool TouchID::isTouchIdAvailable() -{ -#if XC_COMPILER_SUPPORT(TOUCH_ID) - @try { - LAContext *context = [[LAContext alloc] init]; - - LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - NSError *error; - - bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error]; - [context release]; - if (error) { - debug("Touch ID is not available: %s", error.localizedDescription.UTF8String); - } - return canAuthenticate; - } @catch (NSException *) { - return false; - } -#else - return false; -#endif -} - -bool TouchID::isPasswordFallbackPossible() -{ -#if XC_COMPILER_SUPPORT(TOUCH_ID) - @try { - LAContext *context = [[LAContext alloc] init]; - - LAPolicy policyCode = LAPolicyDeviceOwnerAuthentication; - NSError *error; - - bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error]; - [context release]; - if (error) { - debug("Password fallback available: %d (%ld / %s / %s)", canAuthenticate, - (long)error.code, error.description.UTF8String, - error.localizedDescription.UTF8String); - } else { - debug("Password fallback available: %d", canAuthenticate); - } - return canAuthenticate; - } @catch (NSException *) { - return false; - } -#else - return false; -#endif -} - -//! @return true if either TouchID or Apple Watch is available at the moment. -bool TouchID::isAvailable() const -{ - // note: we cannot cache the check results because the configuration - // is dynamic in its nature. User can close the laptop lid or take off - // the watch, thus making one (or both) of the authentication types unavailable. - return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible(); -} - -/** - * Resets the inner state either for all or for the given database - */ -void TouchID::reset(const QUuid& dbUuid) -{ - deleteKeyEntry(databaseKeyName(dbUuid)); -} diff --git a/src/quickunlock/WindowsHello.cpp b/src/quickunlock/WindowsHello.cpp index 7290ac73c..539f00c64 100644 --- a/src/quickunlock/WindowsHello.cpp +++ b/src/quickunlock/WindowsHello.cpp @@ -17,7 +17,7 @@ #include "WindowsHello.h" -#include +#include #include #include #include @@ -29,6 +29,7 @@ #include "crypto/CryptoHash.h" #include "crypto/Random.h" #include "crypto/SymmetricCipher.h" +#include "gui/osutils/OSUtils.h" #include #include @@ -45,17 +46,20 @@ namespace const std::wstring s_winHelloKeyName{L"keepassxc_winhello"}; int g_promptFocusCount = 0; - void queueSecurityPromptFocus(int delay = 500) + void queueSecurityPromptFocus(bool initial, int delay = 500) { + if (initial) { + g_promptFocusCount = 0; + } + QTimer::singleShot(delay, [] { auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr); if (hWnd) { ::SetForegroundWindow(hWnd); } else if (++g_promptFocusCount <= 3) { - queueSecurityPromptFocus(); - return; + qDebug("WindowsHello - Could not find security prompt window"); + queueSecurityPromptFocus(false); } - g_promptFocusCount = 0; }); } @@ -99,47 +103,6 @@ namespace } }); } - - void storeCredential(const QUuid& uuid, const QByteArray& data) - { - auto vault = PasswordVault(); - vault.Add({s_winHelloKeyName, - winrt::to_hstring(uuid.toString().toStdString()), - winrt::to_hstring(data.toBase64().toStdString())}); - } - - void removeCredential(const QUuid& uuid) - { - try { - auto vault = PasswordVault(); - vault.Remove({s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString()), L"nodata"}); - } catch (winrt::hresult_error const& ex) { - } - } - - void resetCredentials() - { - auto vault = PasswordVault(); - auto credentials = vault.FindAllByResource(s_winHelloKeyName); - for (const auto& credential : credentials) { - try { - vault.Remove(credential); - } catch (winrt::hresult_error const& ex) { - } - } - } - - QByteArray loadCredential(const QUuid& uuid) - { - QByteArray data; - try { - auto vault = PasswordVault(); - auto credential = vault.Retrieve(s_winHelloKeyName, winrt::to_hstring(uuid.toString().toStdString())); - data = QByteArray::fromBase64(QByteArray::fromStdString(winrt::to_string(credential.Password()))); - } catch (winrt::hresult_error const& ex) { - } - return data; - } } // namespace bool WindowsHello::isAvailable() const @@ -148,14 +111,9 @@ bool WindowsHello::isAvailable() const return task.get(); } -QString WindowsHello::errorString() const -{ - return m_error; -} - bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data) { - queueSecurityPromptFocus(); + queueSecurityPromptFocus(true); // Generate a random challenge that will be signed by Windows Hello // to create the key. The challenge is also used as the IV. @@ -181,28 +139,28 @@ bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data) // Prepend the challenge/IV to the encrypted data encrypted.prepend(challenge); - storeCredential(dbUuid, encrypted); - return true; + return osUtils->saveSecret(dbUuid.toString(), encrypted); } bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data) { data.clear(); - if (!hasKey(dbUuid)) { - m_error = QObject::tr("Failed to get Windows Hello credential."); + QByteArray keydata; + if (!osUtils->getSecret(dbUuid.toString(), keydata)) { + m_error = QObject::tr("Failed to retrieve Windows Hello credential."); return false; } - queueSecurityPromptFocus(); + queueSecurityPromptFocus(true); // Read the previously used challenge and encrypted data auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); - const auto& keydata = loadCredential(dbUuid); auto challenge = keydata.left(ivSize); auto encrypted = keydata.mid(ivSize); - QByteArray key; + QByteArray key; if (!deriveEncryptionKey(challenge, key, m_error)) { + // Error is set in deriveEncryptionKey return false; } @@ -226,15 +184,16 @@ bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data) void WindowsHello::reset(const QUuid& dbUuid) { - removeCredential(dbUuid); + osUtils->removeSecret(dbUuid.toString()); } bool WindowsHello::hasKey(const QUuid& dbUuid) const { - return !loadCredential(dbUuid).isEmpty(); + QByteArray tmp; + return osUtils->getSecret(dbUuid.toString(), tmp); } void WindowsHello::reset() { - resetCredentials(); + osUtils->removeAllSecrets(); } diff --git a/src/quickunlock/WindowsHello.h b/src/quickunlock/WindowsHello.h index 0f6008590..cff2fe13b 100644 --- a/src/quickunlock/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -26,7 +26,6 @@ public: WindowsHello() = default; bool isAvailable() const override; - QString errorString() const override; bool setKey(const QUuid& dbUuid, const QByteArray& key) override; bool getKey(const QUuid& dbUuid, QByteArray& key) override; @@ -36,8 +35,6 @@ public: void reset() override; private: - QString m_error; - Q_DISABLE_COPY(WindowsHello) }; diff --git a/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml b/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml index d46d71d2a..b8f5328f8 100644 --- a/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml +++ b/src/quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml @@ -12,5 +12,10 @@ + + + + +