TouchID support refactoring (#8311)

Fixes #7695 - Properly set compile flags based on availability of watch unlock in the API.
This commit is contained in:
Dennis 2022-09-05 16:38:02 +02:00 committed by GitHub
parent 15b9e82f93
commit bd809ba90b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 232 additions and 177 deletions

View File

@ -62,6 +62,25 @@ if(UNIX AND NOT APPLE)
endif() endif()
option(WITH_XC_DOCS "Enable building of documentation" ON) option(WITH_XC_DOCS "Enable building of documentation" ON)
if(APPLE)
# Perform the platform checks before applying the stricter compiler flags.
# Otherwise the kSecAccessControlTouchIDCurrentSet deprecation warning will result in an error.
try_compile(XC_APPLE_COMPILER_SUPPORT_BIOMETRY
${CMAKE_CURRENT_BINARY_DIR}/tiometry_test/
${CMAKE_CURRENT_SOURCE_DIR}/cmake/compiler-checks/macos/control_biometry_support.mm)
message(STATUS "Biometry compiler support: ${XC_APPLE_COMPILER_SUPPORT_BIOMETRY}")
try_compile(XC_APPLE_COMPILER_SUPPORT_TOUCH_ID
${CMAKE_CURRENT_BINARY_DIR}/touch_id_test/
${CMAKE_CURRENT_SOURCE_DIR}/cmake/compiler-checks/macos/control_touch_id_support.mm)
message(STATUS "Touch ID compiler support: ${XC_APPLE_COMPILER_SUPPORT_TOUCH_ID}")
try_compile(XC_APPLE_COMPILER_SUPPORT_WATCH
${CMAKE_CURRENT_BINARY_DIR}/tiometry_test/
${CMAKE_CURRENT_SOURCE_DIR}/cmake/compiler-checks/macos/control_watch_support.mm)
message(STATUS "Apple watch compiler support: ${XC_APPLE_COMPILER_SUPPORT_WATCH}")
endif()
if(WITH_CCACHE) if(WITH_CCACHE)
# Use the Compiler Cache (ccache) program # Use the Compiler Cache (ccache) program
# (install with: sudo apt get ccache) # (install with: sudo apt get ccache)

View File

@ -0,0 +1,5 @@
#include <Security/Security.h>
int main() {
return static_cast<int>(kSecAccessControlBiometryCurrentSet);
}

View File

@ -0,0 +1,5 @@
#include <Security/Security.h>
int main() {
return static_cast<int>(kSecAccessControlTouchIDCurrentSet);
}

View File

@ -0,0 +1,5 @@
#include <Security/Security.h>
int main() {
return static_cast<int>(kSecAccessControlWatch);
}

View File

@ -313,7 +313,7 @@ endif()
if(APPLE) if(APPLE)
list(APPEND keepassx_SOURCES touchid/TouchID.mm) list(APPEND keepassx_SOURCES touchid/TouchID.mm)
# TODO: Remove -Wno-error once deprecation warnings have been resolved. # TODO: Remove -Wno-error once deprecation warnings have been resolved.
set_source_files_properties(touchid/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast -Wno-error") set_source_files_properties(touchid/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast")
endif() endif()
configure_file(config-keepassx.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keepassx.h) configure_file(config-keepassx.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keepassx.h)

View File

@ -37,4 +37,13 @@
#cmakedefine HAVE_RLIMIT_CORE 1 #cmakedefine HAVE_RLIMIT_CORE 1
#cmakedefine HAVE_PT_DENY_ATTACH 1 #cmakedefine HAVE_PT_DENY_ATTACH 1
#cmakedefine01 XC_APPLE_COMPILER_SUPPORT_BIOMETRY()
#cmakedefine01 XC_APPLE_COMPILER_SUPPORT_TOUCH_ID()
#cmakedefine01 XC_APPLE_COMPILER_SUPPORT_WATCH()
#define XC_COMPILER_SUPPORT(X) XC_COMPILER_SUPPORT_PRIVATE_DEFINITION_##X()
#define XC_COMPILER_SUPPORT_PRIVATE_DEFINITION_APPLE_BIOMETRY() XC_APPLE_COMPILER_SUPPORT_BIOMETRY()
#define XC_COMPILER_SUPPORT_PRIVATE_DEFINITION_TOUCH_ID() XC_APPLE_COMPILER_SUPPORT_TOUCH_ID()
#define XC_COMPILER_SUPPORT_PRIVATE_DEFINITION_WATCH_UNLOCK() XC_APPLE_COMPILER_SUPPORT_WATCH()
#endif // KEEPASSX_CONFIG_KEEPASSX_H #endif // KEEPASSX_CONFIG_KEEPASSX_H

View File

@ -1,10 +1,6 @@
#ifndef KEEPASSX_TOUCHID_H #ifndef KEEPASSX_TOUCHID_H
#define KEEPASSX_TOUCHID_H #define KEEPASSX_TOUCHID_H
#define TOUCHID_UNDEFINED -1
#define TOUCHID_AVAILABLE 1
#define TOUCHID_NOT_AVAILABLE 0
#include <QHash> #include <QHash>
class TouchID class TouchID
@ -15,30 +11,29 @@ public:
private: private:
TouchID() TouchID()
{ {
// Nothing to do here
} }
// TouchID(TouchID const&); // Don't Implement
// void operator=(TouchID const&); // Don't implement
QHash<QString, QByteArray> m_encryptedMasterKeys;
int m_available = TOUCHID_UNDEFINED;
public: public:
TouchID(TouchID const&) = delete; TouchID(TouchID const&) = delete;
void operator=(TouchID const&) = delete; void operator=(TouchID const&) = delete;
bool storeKey(const QString& databasePath, const QByteArray& passwordKey); bool storeKey(const QString& databasePath, const QByteArray& passwordKey);
bool getKey(const QString& databasePath, QByteArray& passwordKey) const; bool getKey(const QString& databasePath, QByteArray& passwordKey) const;
bool containsKey(const QString& databasePath) const; bool containsKey(const QString& databasePath) const;
void reset(const QString& databasePath = "");
bool isAvailable(); bool isAvailable();
bool authenticate(const QString& message = "") const; private:
static bool isWatchAvailable();
static bool isTouchIdAvailable();
void reset(const QString& databasePath = ""); static void deleteKeyEntry(const QString& accountName);
static QString databaseKeyName(const QString& databasePath);
private:
QHash<QString, QByteArray> m_encryptedMasterKeys;
}; };
#endif // KEEPASSX_TOUCHID_H #endif // KEEPASSX_TOUCHID_H

View File

@ -1,10 +1,11 @@
#define SECURITY_ACCOUNT_PREFIX QString("KeepassXC_TouchID_Keys_")
#include "touchid/TouchID.h" #include "touchid/TouchID.h"
#include "crypto/Random.h" #include "crypto/Random.h"
#include "crypto/SymmetricCipher.h" #include "crypto/SymmetricCipher.h"
#include "crypto/CryptoHash.h" #include "crypto/CryptoHash.h"
#include "config-keepassx.h"
#include <botan/mem_ops.h>
#include <Foundation/Foundation.h> #include <Foundation/Foundation.h>
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
@ -13,16 +14,40 @@
#include <QCoreApplication> #include <QCoreApplication>
inline void debug(const char* message, ...) #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); Q_UNUSED(message);
// qWarning(...); }
#endif
inline std::string StatusToErrorMessage(OSStatus status)
{
CFStringRef text = SecCopyErrorMessageString(status, NULL);
if (!text) {
return std::to_string(status);
}
std::string result(CFStringGetCStringPtr(text, kCFStringEncodingUTF8));
CFRelease(text);
return result;
} }
inline QString hash(const QString& value) inline void LogStatusError(const char *message, OSStatus status)
{ {
QByteArray result = CryptoHash::hash(value.toUtf8(), CryptoHash::Sha256).toHex(); if (!status) {
return QString(result); return;
}
std::string msg = StatusToErrorMessage(status);
debug("%s: %s", message, msg.c_str());
}
inline CFMutableDictionaryRef makeDictionary() {
return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
} }
/** /**
@ -35,23 +60,42 @@ TouchID& TouchID::getInstance()
return instance; return instance;
} }
//! 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::storeKey - Status deleting existing entry", status);
}
QString TouchID::databaseKeyName(const QString &databasePath)
{
static const QString keyPrefix = "KeepassXC_TouchID_Keys_";
const QByteArray pathHash = CryptoHash::hash(databasePath.toUtf8(), CryptoHash::Sha256).toHex();
return keyPrefix + pathHash;
}
/** /**
* Generates a random AES 256bit key and uses it to encrypt the PasswordKey that * 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 * protects the database. The encrypted PasswordKey is kept in memory while the
* AES key is stored in the macOS KeyChain protected by TouchID. * AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch.
*/ */
bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKey) bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKey)
{ {
if (databasePath.isEmpty() || passwordKey.isEmpty()) { if (databasePath.isEmpty() || passwordKey.isEmpty()) {
// illegal arguments debug("TouchID::storeKey - illegal arguments");
debug("TouchID::storeKey - Illegal arguments: databasePath = %s, len(passwordKey) = %d",
databasePath.toUtf8().constData(),
passwordKey.length());
return false; return false;
} }
if (this->m_encryptedMasterKeys.contains(databasePath)) { if (m_encryptedMasterKeys.contains(databasePath)) {
// already stored key for this database
debug("TouchID::storeKey - Already stored key for this database"); debug("TouchID::storeKey - Already stored key for this database");
return true; return true;
} }
@ -62,59 +106,45 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
SymmetricCipher aes256Encrypt; SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
debug("TouchID::storeKey - Error initializing encryption: %s", debug("TouchID::storeKey - AES initialisation falied");
aes256Encrypt.errorString().toUtf8().constData());
return false; return false;
} }
// encrypt and keep result in memory // encrypt and keep result in memory
QByteArray encryptedMasterKey = passwordKey; QByteArray encryptedMasterKey = passwordKey;
if (!aes256Encrypt.finish(encryptedMasterKey)) { if (!aes256Encrypt.finish(encryptedMasterKey)) {
debug("TouchID::storeKey - Error encrypting: %s", aes256Encrypt.errorString().toUtf8().constData()); debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData());
debug(aes256Encrypt.errorString().toUtf8().constData());
return false; return false;
} }
// memorize which database the stored key is for const QString keyName = databaseKeyName(databasePath);
m_encryptedMasterKeys.insert(databasePath, encryptedMasterKey);
NSString* accountName = (SECURITY_ACCOUNT_PREFIX + hash(databasePath)).toNSString(); // autoreleased deleteKeyEntry(keyName); // Try to delete the existing key entry
// try to delete an existing entry
CFMutableDictionaryRef
query = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
// get data from the KeyChain
OSStatus status = SecItemDelete(query);
debug("TouchID::storeKey - Status deleting existing entry: %d", status);
// prepare adding secure entry to the macOS KeyChain // prepare adding secure entry to the macOS KeyChain
CFErrorRef error = NULL; CFErrorRef error = NULL;
SecAccessControlRef sacObject;
#if __clang_major__ >= 9 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
if (@available(macOS 10.15, *)) {
// kSecAccessControlWatch is only available for macOS 10.15 and later
sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAccessControlOr | kSecAccessControlBiometryCurrentSet | kSecAccessControlWatch,
&error);
} else {
#endif
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 101201
sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAccessControlTouchIDCurrentSet, // depr: kSecAccessControlBiometryCurrentSet,
&error);
#endif
#if __clang_major__ >= 9 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
}
#endif
// 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
SecAccessControlCreateFlags accessControlFlags = 0;
if (isTouchIdAvailable()) {
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
// Prefer the non-deprecated flag when available
accessControlFlags = kSecAccessControlBiometryCurrentSet;
#elif XC_COMPILER_SUPPORT(TOUCH_ID)
accessControlFlags = kSecAccessControlTouchIDCurrentSet;
#endif
}
if (isWatchAvailable()) {
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
#endif
}
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(
kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
if (sacObject == NULL || error != NULL) { if (sacObject == NULL || error != NULL) {
NSError* e = (__bridge NSError*) error; NSError* e = (__bridge NSError*) error;
@ -122,36 +152,40 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
return false; return false;
} }
CFMutableDictionaryRef attributes = NSString *accountName = keyName.toNSString(); // The NSString is released by Qt
CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
// prepare data (key) to be stored // prepare data (key) to be stored
QByteArray dataBytes = (randomKey + randomIV).toHex(); QByteArray keychainKeyValue = (randomKey + randomIV).toHex();
CFDataRef keychainValueData =
CFDataRef valueData = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast<UInt8 *>(keychainKeyValue.data()),
CFDataCreateWithBytesNoCopy(NULL, reinterpret_cast<UInt8*>(dataBytes.data()), dataBytes.length(), NULL); keychainKeyValue.length(), kCFAllocatorDefault);
CFMutableDictionaryRef attributes = makeDictionary();
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword); CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName); CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
CFDictionarySetValue(attributes, kSecValueData, valueData); CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData);
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse); CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow); CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject); CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
// add to KeyChain // add to KeyChain
status = SecItemAdd(attributes, NULL); OSStatus status = SecItemAdd(attributes, NULL);
LogStatusError("TouchID::storeKey - Status adding new entry", status);
debug("TouchID::storeKey - Status adding new entry: %d", status); // read w/ e.g. "security error -50" in shell
CFRelease(sacObject); CFRelease(sacObject);
CFRelease(attributes); CFRelease(attributes);
if (status != errSecSuccess) { if (status != errSecSuccess) {
debug("TouchID::storeKey - Not successful, resetting TouchID");
this->m_encryptedMasterKeys.remove(databasePath);
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
m_encryptedMasterKeys.insert(databasePath, encryptedMasterKey);
debug("TouchID::storeKey - Success!");
return true; return true;
} }
@ -163,25 +197,23 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
{ {
passwordKey.clear(); passwordKey.clear();
if (databasePath.isEmpty()) { if (databasePath.isEmpty()) {
// illegal arguments debug("TouchID::getKey - missing database path");
debug("TouchID::storeKey - Illegal argument: databasePath = %s", databasePath.toUtf8().constData());
return false; return false;
} }
// checks if encrypted PasswordKey is available and is stored for the given database
if (!containsKey(databasePath)) { if (!containsKey(databasePath)) {
debug("TouchID::getKey - No stored key found"); debug("TouchID::getKey - No stored key found");
return false; return false;
} }
// query the KeyChain for the AES key // query the KeyChain for the AES key
CFMutableDictionaryRef CFMutableDictionaryRef query = makeDictionary();
query = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
NSString* accountName = (SECURITY_ACCOUNT_PREFIX + hash(databasePath)).toNSString(); // autoreleased const QString keyName = databaseKeyName(databasePath);
NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
NSString* touchPromptMessage = NSString* touchPromptMessage =
QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database") QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
.toNSString(); // autoreleased .toNSString(); // The NSString is released by Qt
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName); CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
@ -198,14 +230,14 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
debug("TouchID::getKey - User canceled authentication"); debug("TouchID::getKey - User canceled authentication");
return true; return true;
} else if (status != errSecSuccess || dataTypeRef == NULL) { } else if (status != errSecSuccess || dataTypeRef == NULL) {
debug("TouchID::getKey - Error retrieving result: %d", status); LogStatusError("TouchID::getKey - key query error", status);
return false; return false;
} }
CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef); CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)), QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
CFDataGetLength(valueData))); CFDataGetLength(valueData)));
CFRelease(valueData); CFRelease(dataTypeRef);
// extract AES key and IV from data bytes // extract AES key and IV from data bytes
QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM)); QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
@ -213,7 +245,7 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
SymmetricCipher aes256Decrypt; SymmetricCipher aes256Decrypt;
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) { if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
debug("TouchID::getKey - Error initializing decryption: %s", aes256Decrypt.errorString().toUtf8().constData()); debug("TouchID::getKey - AES initialization failed");
return false; return false;
} }
@ -221,10 +253,14 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
passwordKey = m_encryptedMasterKeys[databasePath]; passwordKey = m_encryptedMasterKeys[databasePath];
if (!aes256Decrypt.finish(passwordKey)) { if (!aes256Decrypt.finish(passwordKey)) {
passwordKey.clear(); passwordKey.clear();
debug("TouchID::getKey - Error decryption: %s", aes256Decrypt.errorString().toUtf8().constData()); debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
return false; 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; return true;
} }
@ -233,96 +269,77 @@ bool TouchID::containsKey(const QString& dbPath) const
return m_encryptedMasterKeys.contains(dbPath); return m_encryptedMasterKeys.contains(dbPath);
} }
/** // TODO: Both functions below should probably handle the returned errors to
* Dynamic check if TouchID is available on the current machine. // 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
bool TouchID::isAvailable() // 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 MAC_OS_X_VERSION_MIN_REQUIRED < 101201 #if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
return false;
#else
// cache result
if (this->m_available != TOUCHID_UNDEFINED) {
return (this->m_available == TOUCHID_AVAILABLE);
}
@try { @try {
LAContext* context = [[LAContext alloc] init]; LAContext *context = [[LAContext alloc] init];
LAPolicy policyCode; LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
#if __clang_major__ >= 9 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101500 NSError *error;
if (@available(macOS 10.15, *)) {
policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometricsOrWatch;
} else {
#endif
policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
#if __clang_major__ >= 9 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
}
#endif
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:nil]; bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
[context release]; [context release];
this->m_available = canAuthenticate ? TOUCHID_AVAILABLE : TOUCHID_NOT_AVAILABLE; if (error) {
return canAuthenticate; 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);
} }
@catch (NSException*) { return canAuthenticate;
this->m_available = TOUCHID_NOT_AVAILABLE; } @catch (NSException *) {
return false; return false;
} }
#else
return false;
#endif #endif
} }
typedef enum //! @return true if Touch ID is available for authentication.
bool TouchID::isTouchIdAvailable()
{ {
kTouchIDResultNone, #if XC_COMPILER_SUPPORT(TOUCH_ID)
kTouchIDResultAllowed,
kTouchIDResultFailed
} TouchIDResult;
/**
* Performs a simple authentication using TouchID.
*/
bool TouchID::authenticate(const QString& message) const
{
// message must not be an empty string
QString msg = message;
if (message.length() == 0)
msg = QCoreApplication::translate("DatabaseOpenWidget", "authenticate a privileged operation");
@try { @try {
LAContext* context = [[LAContext alloc] init]; LAContext *context = [[LAContext alloc] init];
__block TouchIDResult result = kTouchIDResultNone;
NSString* authMessage = msg.toNSString(); // autoreleased
LAPolicy policyCode; LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
#if __clang_major__ >= 9 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101500 NSError *error;
if (@available(macOS 10.15, *)) {
policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometricsOrWatch;
} else {
#endif
#if MAC_OS_X_VERSION_MIN_REQUIRED >= 101201
policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
#endif
#if __clang_major__ >= 9 && MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
}
#endif
[context evaluatePolicy:policyCode
localizedReason:authMessage reply:^(BOOL success, NSError* error) {
Q_UNUSED(error);
result = success ? kTouchIDResultAllowed : kTouchIDResultFailed;
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}];
while (result == kTouchIDResultNone)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);
bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
[context release]; [context release];
return result == kTouchIDResultAllowed; 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);
} }
@catch (NSException*) { return canAuthenticate;
} @catch (NSException *) {
return false; return false;
} }
#else
return false;
#endif
}
//! @return true if either TouchID or Apple Watch is available at the moment.
bool TouchID::isAvailable()
{
// 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.
const bool watchAvailable = isWatchAvailable();
const bool touchIdAvailable = isTouchIdAvailable();
return watchAvailable || touchIdAvailable;
} }
/** /**
@ -331,9 +348,9 @@ bool TouchID::authenticate(const QString& message) const
void TouchID::reset(const QString& databasePath) void TouchID::reset(const QString& databasePath)
{ {
if (databasePath.isEmpty()) { if (databasePath.isEmpty()) {
this->m_encryptedMasterKeys.clear(); m_encryptedMasterKeys.clear();
return; return;
} }
this->m_encryptedMasterKeys.remove(databasePath); m_encryptedMasterKeys.remove(databasePath);
} }