Password-protected entries in iOS keychain
Quite often an iOS app needs to store some sensitive data like an API access token or a local DB encryption key. Storing this kind of data on filesystem or in UserDefaults definitely isn’t a safest decision. The best option is to use iOS keychain — a secure storage system specifically designed for such cases.
iOS keychain allows each application to store small bits of its private data as a set of entries, each entry having it own name and access rules.
In this article we’re going to talk about creating and later accessing keychain entries protected by a password.
This feature was introduces in iOS 9. You can find out more about new keychain API capabilities introduced in iOS 9 here:
Password-protected entries may be useful when you want user to enter a password to access some local data. For example you can encrypt your database and store the encryption key in a keychain entry (if for some reason you don’t want to directly derive the encryption key from the password).
Also it can be useful as a fallback option for the case when biometry (Touch ID or Face ID) is unavailable (disabled by user) but you still want more security, than provided by a normal keychain entry, whose contents are available to the app always or all the time the app is unlocked.
What do we need to create a password-protected keychain entry? Here are the steps:
- Create an
LAContext
instance and callsetCredential
on it with your password as an argument; - Create a
SecAccessControl
instance for our keychain entry indicating that we want it to be protected by a password; - Put the
LAContext
andSecAccessControl
instances into a dictionary, add some other required values and callSecItemAdd
with that dictionary as the first argument.
Here’s how it looks:
Here our SecAccessControl
instance defines the conditions (user-provided password requirement) under which the app can access the keychain entry.
In this Apple doc you can find more info about available ways of protecting keychain items:
Now, how can we read that entry? There are several slightly different ways to do it. They all require a SecItemCopyMatching
function call, the difference is only in parameters we supply to that function. To demonstrate all these approaches we will be using the following method:
It prepares all the required arguments for SecItemCopyMatching
and calls the function to read the keychain entry data.
Simple SecItemCopyMatching call
Let’s look at the first way how we can use this method. Here we won’t be creating and supplying our own LAContext
instance, we will use nil
instead. So in our query
dictionary kSecUseAuthenticationContext
and kSecUseAuthenticationUI
keys will be missing:
When SecItemCopyMatching
is called, iOS displays a special UI prompt asking for the password. It looks like this:
Instead of “keychain-sample” you’ll have your app’s name.
Note that in this case the SecItemCopyMatching
call is synchronous and it has to be done on a background thread to prevent the main thread from blocking.
If user enters a wrong password then iOS automatically re-displays the prompt. SecItemCopyMatching
call won’t return until user enters the correct password or taps “Cancel” or exceeds the maximum allowed number of attempts (defined by the system, in my case it was 5 if I’m not mistaken).
Using LAContext.evaluateAccessControl
If we want to control the number of attempts manually and also avoid the long-running blocking call SecItemCopyMatching
we can use a slightly different approach:
Here we create an LAContext
instance and call evaluateAccessControl
with operation: .useItem
for it. This evaluateAccessControl
call will trigger the same UI prompt as we had previously. Subsequent SecItemCopyMatching
call won’t be blocking however.
Also it is worth noting that the localizedReason
parameter for some reason is ignored in this evaluateAccessControl
call and the UI prompt looks exactly the same as in the previous case.
The key point in preparing the data for the SecItemCopyMatching
is to set our LAContext
instance as a value for the kSecUseAuthenticationContext
key and also set kSecUseAuthenticationUIFail
value for kSecUseAuthenticationUI
.
In this case our loadPassProtected
call will fail after the first wrong password attempt and we’ll have to repeat the procedure manually if we want to allow the second attempt. Thus we can control the number of attempts.
By the way, if we don’t use kSecUseAuthenticationUIFail
in the SecItemCopyMatching
call, the system will automatically handle wrong passwords the same way as we had it before. And SecItemCopyMatching
will become blocking.
Custom UI instead of system prompt
Now what if the system password prompt doesn’t play well with our app design? It appears that we can implement our own UI for password input and just supply the password value to the keychain API.
To do this we have to create an LAContext
instance and set the password obtained from user to it. Just the same way as we were doing it when we were saving the entry in keychain:
Here we also use a non-blocking SecItemCopyMatching
call.
It also should be noted that you need a real device to test this code. It doesn’t work in simulator. I’ve seen mentions that it won’t work on a real device with passcode turned off, here for example:
But my tests show that at least in iOS 12 it works fine when I turn off the passcode on my device.
Sample code for creating and reading password-protected entries as described in this post can be found in this project on github:
I hope this information can be useful to make your iOS app more secure. Please let me know if you find an issue or have any suggestions.