Image copyrights and trademarks belong exclusively to Apple. #What is Touch ID?
Simply put, Touch ID is Apple’s fingerprint technology for iOS mobile devices. It allows consumers to unlock their phones and make purchases conveniently using their fingerprint(s). Furthermore, as of iOS version 8.0, Apple opened Touch ID up to developers by making APIs available for use in the SDK.
###Biometric opinions
This post assumes you have performed your own risk assessment and are aware of the risks associated with biometric authentication technologies, and that you have decided that Touch ID is suitable for use in your application. ###Why this post then? The reason for this post is simple - I want to provide some information to allow software architects and developers to better understand Touch ID, the ways it can be included in your iOS applications and what the security benefits to the different approaches are. These are all questions I hear regularly when providing iOS security consultancy.
The LocalAuthentication framework is not the only set of APIs to be considered when implementing Touch ID in to your iOS software, this post will cover the others too.
###Approach 1 - LocalAuthentication.framework
The first and most commonly used approach to integrating Touch ID into your iOS applications is to utilize the LocalAuthentication framework. The framework exposes a class called LAContext
which allows you to:
A) Verify that the device is Touch ID capable and ready.
B) Present an authentication dialog to the user and evaluate whether or not their provided fingerprint matched successfully or not.
This can be observed below in both Objective-C and Swift:-
######Objective-C
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
NSString *reason = @"Please authenticate using TouchID.";
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:reason
reply:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"Auth was OK");
}
else {
//You should do better handling of error here but I'm being lazy
NSLog(@"Error received: %d", error);
}
}];
}
else {
NSLog(@"Can not evaluate Touch ID");
}
######Swift
var context:LAContext = LAContext();
var error:NSError?
var success:Bool;
var reason:String = "Please authenticate using TouchID.";
if (context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &error))
{
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: reason, reply: { (success, error) -> Void in
if (success) {
println("Auth was OK");
}
else
{
//You should do better handling of error here but I'm being lazy
println("Error received: %d", error!);
}
});
}
###Approach 2 - Access Controls via Keychain Services (Security.framework) The second, less common approach to integrating Touch ID into your iOS applications is to utilize the KeychainServices API set. The Keychain Services APIs allow applications to interact with the on device Keychain (secure storage), providing APIs to delete, store, update and obtain data securely.
When storing data in the keychain, the developers can specify a set of attributes for the data which among other things, can be used to protect the data with user authentication (including Touch ID). Basically, developers can store data that can only be obtained after the user successfully authenticates using Touch ID. The secure enclave is utilized in the design to ensure successful authentication is required.
You can see the diffs for iOS 8 here:- https://developer.apple.com/library/ios/releasenotes/General/iOS80APIDiffs/frameworks/Security.html.
To utilize Touch ID protection for Keychain items, create a security access control reference using the SecAccessControlCreateWithFlags()
API. When using this API, specify the user presence (kSecAccessControlUserPresence
) policy and a protection class of kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
. You can then use the returned SecAccessControlRef
in the attributes dictionary (key: kSecAttrAccessControl
) when inserting the data.
This approach can be observed below in both Objective-C and Swift:-
######Objective-C
#define HEX_SERVICE @"HEX_EXAMPLE_SERVICE"
#define HEX_SERVICE_MSG @"Authenticate to unlock the key"
SecAccessControlRef sacRef;
CFErrorRef *err = nil;
/*
Important considerations.
Please read the docs regarding kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly.
TL;DR - If the user unsets their device passcode, these keychain items are destroyed.
You will need to add code to compensate for this, i.e to say that touch ID can only be used if the device has a passcode set.
Additionally, keychain entries with this flag will not be backed up/restored via iCloud.
*/
//Gets our Security Access Controll ref for user presence policy (requires user AuthN)
sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
kSecAccessControlUserPresence,
err);
NSDictionary *attributes = @{
//Sec class, in this case just a password
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
//Our service UUID/Name
(__bridge id)kSecAttrService: HEX_SERVICE,
//The data to insert
(__bridge id)kSecValueData: [@"sup3r_s3cur3_k3y"
dataUsingEncoding:NSUTF8StringEncoding],
//Whether or not we want to prompt on insert
(__bridge id)kSecUseNoAuthenticationUI: @YES,
//Our security access control reference
(__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacRef
};
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Insert the data to the keychain, using our attributes dictionary
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
});
}
And now to obtain the data from the keychain, use the following:-
/* Lets get our secret from the keychain.
* User will be asked for Touch ID or device passcode if Touch ID not available
* You could use LocalAuthentication's canEvaluatePolicy method to determine if this is a touch ID device first.
*/
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: HEX_SERVICE,
(__bridge id)kSecReturnData: @YES,
(__bridge id)kSecUseOperationPrompt: HEX_SERVICE_MSG
};
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
if (status == errSecSuccess)
{
NSData *resultData = ( __bridge_transfer NSData *)dataTypeRef;
NSString * result = [[NSString alloc]
initWithData:resultData
encoding:NSUTF8StringEncoding];
//Show alertview on main queue
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Keychain entry: %@", result);
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle: @"Thanks"
message:[NSString stringWithFormat:
@"The key is: %@", result]
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
});
}
else
{
//Normally would do better error handling
NSLog(@"Something went wrong");
}
});
######Swift
let service = "HEX_EXAMPLE_SERVICE_RANDOM"
let reason = "Authenticate to unlock the key"
var error: Unmanaged<CFError>?;
let sacRef = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.UserPresence,
&error);
let data: NSData = "sup3r_s3cur3_k3y".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!;
var attributes: NSMutableDictionary = NSMutableDictionary(
objects: [ kSecClassGenericPassword,
service,
data,
kCFBooleanTrue,
sacRef.takeRetainedValue()
],
forKeys: [ kSecClass,
kSecAttrService,
kSecValueData,
kSecUseNoAuthenticationUI,
kSecAttrAccessControl]);
var status: OSStatus = SecItemAdd(attributes as CFDictionaryRef, nil);
And now to obtain the data from the keychain, use the following:-
var query: NSMutableDictionary = NSMutableDictionary(
objects: [ kSecClassGenericPassword,
service,
kCFBooleanTrue,
kSecMatchLimitOne,
reason],
forKeys: [ kSecClass,
kSecAttrService,
kSecReturnData,
kSecMatchLimit,
kSecUseOperationPrompt])
var dataTypeRef :Unmanaged<AnyObject>?
let status: OSStatus = SecItemCopyMatching(query, &dataTypeRef)
if (status == errSecSuccess)
{
let resultData = dataTypeRef?.takeRetainedValue() as! NSData?;
println("Keychain entry: \(NSString(data: resultData!, encoding: NSUTF8StringEncoding))");
} else {
//Normally would do better error handling
println("Something went wrong");
}
###Conclusion - Which approach is better? The answer is that it depends on your threat model. Using keychain services certainly offers stronger security but LocalAuthentication will allow you to very easily and quickly integrate Touch ID into your iOS applications.
For most applications, the LocalAuthentication framework should be sufficient if you’re looking to support Touch ID authentication. The trouble is that LocalAuthentication as a control can be trivially bypassed via hooking the APIs (e.g. via Cydia Substrate). I.e. if the device is jailbroken, the attacker will be able to circumvent Local Authentication with ease and access the application as the user/victim.
For higher risk apps (financial, medical, insurance, etc.) however, Keychain Services should be the approach you take. Although keychain APIs are generally considered harder to work with, the time it takes to write a neat wrapper (to make your life easier) is worth investing in.
If your concern is root level malware, the data is at risk regardless of which approach you take since malware can simply sit dormant until the user unlocks the data and steal it then. However, if your concern is device theft and you’re using keychain services, the thief will not be able to unlock the data easily. At least, the attacker will not be able to simply hook an API to circumvent the control. This is because the data stored within the keychain entry will not be accessible until the user is successfully authenticated (via passcode/Touch ID).