Biometry-protected entries in iOS keychain
Keychain API lets your app to store small bits of sensitive information in a secure way. One can set different access control settings for every keychain entry thus defining a set of requirements that have to be met to access it. In my previous post I described how to protect a keychain entry with a user-provided password:
Now we’re going to talk about protecting keychain entries with biometry i.e. requiring user to go through Face ID or Touch ID authentication to access the data.
To be able to use this kind of access control settings for our keychain entry we need to have fingerprints or a Face ID scan enrolled on our iOS device. To check that it is all set up and enabled we can use the canEvaluatePolicy
method of the LAContext
class. Here’s one way to do it:
In this snippet we distinguish the following three states:
available
— everything is fine and we’re ready to use biometric authentication in our applocked
— biometric authentication is currently disabled (see below)notAvailable
— biometric authentication is not available
If biometric authentication is not available user can enable it by going to iOS settings, setting up system passcode (if not set) and enrolling a fingerprint or scanning his/her face with Face ID.
It is also important to be aware of the locked
state. After five consecutive unsuccessful Face ID or Touch ID authentication attempts the biometric authentication becomes disabled. To re-enable it user is required to enter the device passcode. Below we will show how to implement this in code.
An important thing to mention regarding Face ID: you have to add a usage description string to your Info.plist
file. Otherwise the authentication won’t work on devices with Face ID. The name of the corresponding key is NSFaceIDUsageDescription
. For Touch ID, however, you don’t need such a string.
Creating a biometry-protected entry
Let’s see now how to create a biometry-protected keychain item. This is relatively easy. As for any other keychain entry we have to use the SecItemAdd
call:
The main point here is to set a special SecAccessControl
instance for the kSecAttrAccessControl
key when preparing the query
dictionary parameter for the SecItemAdd
call. To create that instance we call SecAccessControlCreateWithFlags
with the following parameters:
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
— our keychain entry can only be read when the iOS device is unlocked. Also it won’t be copied to other devices via iCloud and won’t be added to backups..biometryCurrentSet
— sets the requirement of Touch ID or Face ID authentication. It strictly ties your entry to the currently enrolled biometric data. Note that prior to iOS 11.3 it was.touchIDCurrentSet
. This flag doesn’t allow fallback to passcode instead of fingerprint or face scan, if you want that you can use.userPresence
instead.
Reading a biometry-protected entry
Now how can we read the entry we just created? Just the same as in the case with password-protected entries we have a couple of slightly different options here. The first option is to call SecItemCopyMatching
synchronously and let it present the Touch ID or Face ID authentication UI. The second option is to use an LAContext
instance for the authentication.
Before using biometric authentication it’s important to make sure that our iOS device is ready for that. The biometry must be turned on and fingerprints or face scan must be present. To perform this check we can use a method like this:
Here we’re using the biometryState
property that we have defined at the start of this article.
If biometric authentication is temporarily disabled we need to ask user to unblock it by entering the device passcode. We do this by calling authContext.evaluatePolicy
with deviceOwnerAuthentication
value passed as the first argument (this is the policy we’re interested in). It triggers the device passcode input screen.
Note that evaluatePolicy
calls its callback parameter on a background thread. So if we want to execute some UI code in there we need to put it in a DispatchQueue.main.async
call.
When we’re sure that biometric authentication is available we can go further and read the entry contents. For this we’ll be using the following method:
The kSecUseOperationPrompt
key here is used to set the message that will be displayed inside the system biometric authentication prompt. Note that it is only used when you follow the first approach described below. For the second approach (the one with LAContext.evaluateAccessControl
) the localizedReason
parameter of that call will be used for the same purpose.
First approach: without an LAContext
Let’s look at the first way to read the entry where we call SecItemCopyMatching
without an LAContext
instance in its parameters (inside the query
dictionary). This corresponds to a loadBioProtected
call with the context
parameter set to nil
:
This is a simple example reading a string value from an entry with name stored in a constant named entryName
and calling a sample method showStatus
to display the result to user.
Here SecItemCopyMatching
will be called synchronously so we’re doing it on a background thread to prevent the main thread from blocking. The call starts Touch ID or Face ID authentication process and returns only when it finishes. If the authentication was successful the function returns 0 (noErr
). If something went wrong it returns an error code. Here are few possible codes that can be returned by SecItemCopyMatching
:
- -128 (
errSecUserCanceled
) — authentication process was cancelled by user - -25300 (
errSecItemNotFound
) — keychain entry wasn’t found - -25293 (
errSecAuthFailed
) — authentication failed
Second approach: using LAContext.evaluateAccessControl
As an option we can perform biometric authentication for an LAContext
instance thus avoiding the blocking SecItemCopyMatching
call. To do this we need a new LAContext
instance and a SecAccessControl
instance which can be created with the help of the getBioSecAccessControl
method defined above.
Then we call the authContext.evaluateAccessControl
method prompting user for Touch ID or Face ID authentication. Note that evaluateAccessControl
calls its callback on a background thread so don’t forget to call DispatchQueue.main.async
where appropriate.
If the authentication succeeds we use our authContext
instance to read the keychain entry contents:
When loadBioProtected
is called our authContext
instance is put into the query
dictionary for the kSecUseAuthenticationContext
key — this ensures that previously performed authentication is taken into account by the subsequent SecItemCopyMatching
call.
Final notes
I hope that this information and code samples can be helpful for those who’re planning to use iOS keychain in their apps. Full sample project code is available on github:
Note that the keychain API doesn’t fully work on iOS simulator. You need a real device to use it. The API works closely with the secure enclave hardware and it’s not fully implemented in the simulator.
Another important thing that should be remembered: when user changes his/her security settings like changing passcode, adding or deleting fingerprints or Face ID scans, all the biometry-protected entries are removed (become inaccessible). If you want to keep them (sacrificing somewhat on security) you can use the .biometryAny
(.touchIDAny
for older iOS versions) flag instead of .biometryCurrentSet
when creating your SecAccessControl
instance (see getBioSecAccessControl
method defined above).
For further information you can look at the official Apple documentation here: