Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b95f964
Added kSecAccessControlDevicePasscode to accessControlflags when feat…
findus Oct 24, 2024
255b44e
Included check in isAvailable method
findus Oct 24, 2024
261ce02
Added toggle button in settings to disable fallback (default: disabled)
findus Oct 24, 2024
7c59c5c
Added config check to appropriate method
findus Oct 24, 2024
072533d
Use OR to be able to use either biometry or password, if touchid is u…
findus Oct 24, 2024
1cec8ed
Renamed dialog setting text
findus Oct 24, 2024
c33c54a
i8n update
findus Oct 24, 2024
7f3cb0f
Clang format
findus Oct 24, 2024
1e136e5
Formatting fixes
findus Oct 24, 2024
9b563a5
Removed TouchID / WatchID presence check on key write.
findus Oct 25, 2024
7a59e00
Removed setting entry again to toggle password fallback
findus Oct 25, 2024
0e80d00
Removed watchID and touchID availability checks
findus Oct 25, 2024
324fdd9
Clang Format
findus Oct 25, 2024
d524b01
Merge branch 'develop' into feature/touchid-password-fallback
findus Oct 25, 2024
f675cd3
Rephrased comments
findus Oct 25, 2024
f9d4d1a
Make device passcode unlock always present
findus Oct 25, 2024
4e87911
Revert "Removed watchID and touchID availability checks"
findus Oct 25, 2024
6b561cf
Additional check if TouchID is enrolled.
findus Oct 25, 2024
5543eda
Removed config import
findus Oct 26, 2024
649c961
fallback to quick unlock without touchid if saving key fails with sel…
findus Oct 29, 2024
6e8c115
more documentation to why this logic is necessary
findus Oct 29, 2024
15ee4bf
fixed compile error
findus Oct 29, 2024
7aca475
Botan: always scrub memory, also after failed save attempt
findus Nov 5, 2024
bb6829f
remove enrolled check again, not needed anymore, we just call seKey a…
findus Nov 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/quickunlock/TouchID.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ class TouchID : public QuickUnlockInterface
private:
static bool isWatchAvailable();
static bool isTouchIdAvailable();
static bool isPasswordFallbackPossible();
static bool isTouchIdEnrolled();
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID);

static void deleteKeyEntry(const QString& accountName);
static QString databaseKeyName(const QUuid& dbUuid);
Expand Down
124 changes: 106 additions & 18 deletions src/quickunlock/TouchID.mm
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,10 @@ inline CFMutableDictionaryRef makeDictionary() {
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)



bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey, const bool ignoreTouchID)
{
if (passwordKey.isEmpty()) {
debug("TouchID::setKey - illegal arguments");
Expand Down Expand Up @@ -131,22 +129,40 @@ inline CFMutableDictionaryRef makeDictionary() {

// 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 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;
if (isTouchIdAvailable()) {
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
// Prefer the non-deprecated flag when available
accessControlFlags = kSecAccessControlBiometryCurrentSet;
// 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.
//
// 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 (isTouchIdEnrolled() && !ignoreTouchID) {
// Prefer the non-deprecated flag when available
accessControlFlags = kSecAccessControlBiometryCurrentSet;
}
#elif XC_COMPILER_SUPPORT(TOUCH_ID)
accessControlFlags = kSecAccessControlTouchIDCurrentSet;
#endif
if (isTouchIdEnrolled() && !ignoreTouchID) {
accessControlFlags = kSecAccessControlTouchIDCurrentSet;
}
#endif

if (isWatchAvailable()) {
#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);
Expand Down Expand Up @@ -179,21 +195,36 @@ inline CFMutableDictionaryRef makeDictionary() {

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

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

/**
* 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
* decrypt it using the KeyChain and if successful, returns it.
Expand Down Expand Up @@ -332,13 +363,70 @@ inline CFMutableDictionaryRef makeDictionary() {
#endif
}

//! @return true if finger is enrolled
bool TouchID::isTouchIdEnrolled()
{
#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];
//TODO: check if this is the correct error message
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we satisfy this TODO?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also get rid of this check entirely now as we call setKey twice in case of an error

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

if (error && error.code == LAErrorBiometryNotEnrolled) {
debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate,
(long)error.code, error.description.UTF8String,
error.localizedDescription.UTF8String);
} else {
debug("Touch ID not available but enrolled");
canAuthenticate = true;
}
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();
return isWatchAvailable() || isTouchIdAvailable() || isPasswordFallbackPossible();
}

/**
Expand Down