Biometry-protected entries in iOS keychain

Alexei Gridnev
5 min readApr 19, 2019

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 app
  • locked — 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:

--

--