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);