diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 50f6bd48c..99c6d3670 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -343,7 +343,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(); diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 2e38ee2c8..1a6d6adae 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 @@ -327,7 +327,7 @@ 40 - 20 + 0 @@ -447,7 +447,7 @@ 30 - 20 + 0 @@ -477,7 +477,7 @@ 40 - 20 + 0 @@ -533,7 +533,7 @@ 30 - 20 + 0 @@ -566,7 +566,7 @@ 30 - 20 + 0 @@ -628,7 +628,7 @@ 40 - 20 + 0 @@ -734,7 +734,7 @@ 40 - 20 + 0 @@ -813,7 +813,7 @@ 30 - 20 + 0 @@ -858,7 +858,7 @@ 40 - 20 + 0 @@ -915,7 +915,7 @@ 30 - 20 + 0 @@ -970,7 +970,7 @@ 50 - 20 + 0 diff --git a/src/gui/ApplicationSettingsWidgetSecurity.ui b/src/gui/ApplicationSettingsWidgetSecurity.ui index 540681bca..b9f83e57e 100644 --- a/src/gui/ApplicationSettingsWidgetSecurity.ui +++ b/src/gui/ApplicationSettingsWidgetSecurity.ui @@ -138,7 +138,7 @@ 40 - 20 + 0 @@ -174,6 +174,9 @@ + + 0 + @@ -185,7 +188,7 @@ 30 - 20 + 0 diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp index 3b6eb4f3f..e3ff2a98d 100644 --- a/src/gui/DatabaseOpenWidget.cpp +++ b/src/gui/DatabaseOpenWidget.cpp @@ -326,7 +326,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/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 2cca7ea46..8abaf94c4 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 77960beba..10354345d 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) @@ -89,42 +89,19 @@ void TouchID::reset() } /** - * 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. + * 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& passwordKey) +bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& key) { - 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; @@ -152,55 +129,47 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey) 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); - if (status != errSecSuccess) { - return false; - } - - // Cleanse the key information from the memory - Botan::secure_scrub_memory(randomKey.data(), randomKey.size()); - Botan::secure_scrub_memory(randomIV.data(), randomIV.size()); - - // memorize which database the stored key is for - m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); - debug("TouchID::setKey - Success!"); - return true; + return status == errSecSuccess; } /** - * 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"); @@ -235,39 +204,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 @@ -289,11 +249,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 *) { @@ -317,11 +273,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 *) { @@ -341,10 +293,15 @@ bool TouchID::isAvailable() const return isWatchAvailable() || isTouchIdAvailable(); } +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 bc3fee736..b1d7c94b2 100644 --- a/src/quickunlock/WindowsHello.cpp +++ b/src/quickunlock/WindowsHello.cpp @@ -163,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; } @@ -233,6 +234,11 @@ 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 b13dce939..3032b89a9 100644 --- a/src/quickunlock/WindowsHello.h +++ b/src/quickunlock/WindowsHello.h @@ -27,15 +27,19 @@ 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; Q_DISABLE_COPY(WindowsHello);