Device Password fallback when Touch-ID devices are unavailable (#11410)

* Added kSecAccessControlDevicePasscode to accessControlflags when feature is enabled in settings and no biometrics are available

* Able to use either biometry or password, if touchid is unavailable

* Additional check if TouchID is enrolled:

With that we can add the "kSecAccessControlBiometryCurrentSet" Flag even though Biometry is unavailable due to closed lid or unpaired keyboard. Adding this flag when TouchID is not enrolled results in an error when trying to save the secret.

The kSecAccessControlWatch Flag for apple watch compatibility does not have this limitation. With that we can also offer quick unlock with only apple watch or password

* Fallback to quick unlock without touchid if saving key fails with selected flags, might fix quick unlock on a hackintosh
This commit is contained in:
Findus 2024-11-10 23:10:04 +01:00 committed by GitHub
parent ca9b88fae8
commit bff0b93f5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 19 deletions

View File

@ -37,6 +37,8 @@ public:
private: private:
static bool isWatchAvailable(); static bool isWatchAvailable();
static bool isTouchIdAvailable(); static bool isTouchIdAvailable();
static bool isPasswordFallbackPossible();
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID);
static void deleteKeyEntry(const QString& accountName); static void deleteKeyEntry(const QString& accountName);
static QString databaseKeyName(const QUuid& dbUuid); static QString databaseKeyName(const QUuid& dbUuid);

View File

@ -88,12 +88,10 @@ void TouchID::reset()
m_encryptedMasterKeys.clear(); m_encryptedMasterKeys.clear();
} }
/**
* 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, const bool ignoreTouchID)
*/
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
{ {
if (passwordKey.isEmpty()) { if (passwordKey.isEmpty()) {
debug("TouchID::setKey - illegal arguments"); debug("TouchID::setKey - illegal arguments");
@ -131,22 +129,40 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
// We need both runtime and compile time checks here to solve the following problems: // 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 // - Not all flags are available in all OS versions, so we have to check it at compile time
// - Requesting Biometry/TouchID when to fingerprint sensor is available will result in runtime error // - Requesting Biometry/TouchID/DevicePassword when to fingerprint sensor is available will result in runtime error
SecAccessControlCreateFlags accessControlFlags = 0; SecAccessControlCreateFlags accessControlFlags = 0;
if (isTouchIdAvailable()) {
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY) #if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
// Prefer the non-deprecated flag when available // Needs a special check to work with SecItemAdd, when TouchID is not enrolled and the flag
accessControlFlags = kSecAccessControlBiometryCurrentSet; // is set, the method call fails with an error. But we want to still set this flag if TouchID is
#elif XC_COMPILER_SUPPORT(TOUCH_ID) // enrolled but temporarily unavailable due to closed lid
accessControlFlags = kSecAccessControlTouchIDCurrentSet; //
#endif // At least on a Hackintosh the enrolled-check does not work, there LAErrorBiometryNotAvailable gets returned instead of
// LAErrorBiometryNotEnrolled.
//
// Thats 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 (isWatchAvailable()) {
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK) #if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch; accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
#endif #endif
#if XC_COMPILER_SUPPORT(TOUCH_ID)
if (isPasswordFallbackPossible()) {
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlDevicePasscode;
} }
#endif
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags( SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error); kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
@ -179,21 +195,36 @@ bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
CFRelease(sacObject); CFRelease(sacObject);
CFRelease(attributes); 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());
if (status != errSecSuccess) { if (status != errSecSuccess) {
return false; 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 // memorize which database the stored key is for
m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey); m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
debug("TouchID::setKey - Success!"); debug("TouchID::setKey - Success!");
return true; 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;
}
}
/** /**
* Checks if an encrypted PasswordKey is available for the given database, tries to * Checks if an encrypted PasswordKey is available for the given database, tries to
* decrypt it using the KeyChain and if successful, returns it. * decrypt it using the KeyChain and if successful, returns it.
@ -332,13 +363,40 @@ bool TouchID::isTouchIdAvailable()
#endif #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. //! @return true if either TouchID or Apple Watch is available at the moment.
bool TouchID::isAvailable() const bool TouchID::isAvailable() const
{ {
// note: we cannot cache the check results because the configuration // note: we cannot cache the check results because the configuration
// is dynamic in its nature. User can close the laptop lid or take off // 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. // the watch, thus making one (or both) of the authentication types unavailable.
return isWatchAvailable() || isTouchIdAvailable(); return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible();
} }
/** /**