Advertisement
  1. Code
  2. Coding Fundamentals
  3. Security

Securing iOS Data at Rest: The Keychain

Scroll to top
This post is part of a series called Securing iOS Data at Rest.
Securing iOS Data at Rest: Protecting the User's Data
Securing iOS Data at Rest: Encryption

Any app that saves the user's data has to take care of the security and privacy of that data. As we've seen with recent data breaches, there can be very serious consequences for failing to protect your users' stored data. In this tutorial, you'll learn some best practices for protecting your users' data.

In the previous post, you learned how to protect files using the Data Protection API. File-based protection is a powerful feature for secure bulk data storage. But it might be overkill for a small amount of information to protect, such as a key or password. For these types of items, the keychain is the recommended solution.

Keychain Services

The keychain is a great place to store smaller amounts of information such as sensitive strings and IDs that persist even when the user deletes the app. An example might be a device or session token that your server returns to the app upon registration. Whether you call it a secret string or unique token, the keychain refers to all of these items as passwords

There are a few popular third-party libraries for keychain services, such as Strongbox (Swift) and SSKeychain (Objective-C). Or, if you want complete control over your own code, you may wish to directly use the Keychain Services API, which is a C API. 

I will briefly explain how the keychain works. You can think of the keychain as a typical database where you run queries on a table. The functions of the keychain API all require a CFDictionary object that contains attributes of the query. 

Each entry in the keychain has a service name. The service name is an identifier: a key for whatever value you want to store or retrieve in the keychain. To allow a keychain item to be stored for a specific user only, you'll also often want to specify an account name. 

Because each keychain function takes a similar dictionary with many of the same parameters to make a query, you can avoid duplicate code by making a helper function that returns this query dictionary.

1
import Security
2
3
//...

4
5
class func passwordQuery(service: String, account: String) -> Dictionary<String, Any>
6
{
7
    let dictionary = [
8
        kSecClass as String : kSecClassGenericPassword,
9
        kSecAttrAccount as String : account,
10
        kSecAttrService as String : service,
11
        kSecAttrAccessible as String : kSecAttrAccessibleWhenUnlocked //If need access in background, might want to consider kSecAttrAccessibleAfterFirstUnlock

12
    ] as [String : Any]
13
    
14
    return dictionary
15
}

This code sets up the query Dictionary with your account and service names and tells the keychain that we will be storing a password. 

Similarly to how you can set the protection level for individual files (as we discussed in the previous post), you can also set the protection levels for your keychain item using the kSecAttrAccessible key

Adding a Password

The SecItemAdd() function adds data to the keychain. This function takes a Data object, which makes it versatile for storing many kinds of objects. Using the password query function we created above, let's store a string in the keychain. To do this, we just have to convert the String to Data.

1
@discardableResult class func setPassword(_ password: String, service: String, account: String) -> Bool
2
{
3
    var status : OSStatus = -1
4
    if !(service.isEmpty) && !(account.isEmpty)
5
    {
6
        deletePassword(service: service, account: account) //delete password if pass empty string. Could change to pass nil to delete password, etc

7
        
8
        if !password.isEmpty
9
        {
10
            var dictionary = passwordQuery(service: service, account: account)
11
            let dataFromString = password.data(using: String.Encoding.utf8, allowLossyConversion: false)
12
            dictionary[kSecValueData as String] = dataFromString
13
            status = SecItemAdd(dictionary as CFDictionary, nil)
14
        }
15
    }
16
    return status == errSecSuccess
17
}

Deleting a Password

To prevent duplicate inserts, the code above first deletes the previous entry if there is one. Let's write that function now. This is accomplished using the SecItemDelete() function.

1
@discardableResult class func deletePassword(service: String, account: String) -> Bool
2
{
3
    var status : OSStatus = -1
4
    if !(service.isEmpty) && !(account.isEmpty)
5
    {
6
        let dictionary = passwordQuery(service: service, account: account)
7
        status = SecItemDelete(dictionary as CFDictionary);
8
    }
9
    return status == errSecSuccess
10
}

Retrieving a Password

Next, to retrieve an entry from the keychain, use the SecItemCopyMatching() function. It will return an AnyObject that matches your query.

1
class func password(service: String, account: String) -> String //return empty string if not found, could return an optional

2
{
3
    var status : OSStatus = -1
4
    var resultString = ""
5
    if !(service.isEmpty) && !(account.isEmpty)
6
    {
7
        var passwordData : AnyObject?
8
        var dictionary = passwordQuery(service: service, account: account)
9
        dictionary[kSecReturnData as String] = kCFBooleanTrue
10
        dictionary[kSecMatchLimit as String] = kSecMatchLimitOne
11
        status = SecItemCopyMatching(dictionary as CFDictionary, &passwordData)
12
        
13
        if status == errSecSuccess
14
        {
15
            if let retrievedData = passwordData as? Data
16
            {
17
                resultString = String(data: retrievedData, encoding: String.Encoding.utf8)!
18
            }
19
        }
20
    }
21
    return resultString
22
}

In this code, we set the kSecReturnData parameter to kCFBooleanTruekSecReturnData means the actual data of the item will be returned. A different option could be to return the attributes (kSecReturnAttributes) of the item. The key takes a CFBoolean type which holds the constants kCFBooleanTrue or kCFBooleanFalse. We are setting kSecMatchLimit to kSecMatchLimitOne so that only the first item found in the keychain will be returned, as opposed to an unlimited number of results.

Public and Private Keys

The keychain is also the recommended place to store public and private key objects, for example, if your app works with and needs to store EC or RSA SecKey objects. 

The main difference is that instead of telling the keychain to store a password, we can tell it to store a key. In fact, we can get specific by setting the types of keys stored, such as whether it is public or private. All that needs to be done is to adapt the query helper function to work with the type of key you want. 

Keys generally are identified using a reverse domain tag such as com.mydomain.mykey instead of service and account names (since public keys are openly shared between different companies or entities). We will take the service and account strings and convert them to a tag Data object. For example, the above code adapted to store an RSA Private SecKey would look like this:

1
class func keyQuery(service: String, account: String) -> Dictionary<String, Any>
2
{
3
    let tagString = "com.mydomain." + service + "." + account
4
    let tag = tagString.data(using: .utf8)! //Store it as Data, not as a String

5
    let dictionary = [
6
        kSecClass as String : kSecClassKey,
7
        kSecAttrKeyType as String : kSecAttrKeyTypeRSA,
8
        kSecAttrKeyClass as String : kSecAttrKeyClassPrivate,
9
        kSecAttrAccessible as String : kSecAttrAccessibleWhenUnlocked,
10
        kSecAttrApplicationTag as String : tag
11
        ] as [String : Any]
12
    
13
    return dictionary
14
}
15
16
@discardableResult class func setKey(_ key: SecKey, service: String, account: String) -> Bool
17
{
18
    var status : OSStatus = -1
19
    if !(service.isEmpty) && !(account.isEmpty)
20
    {
21
        deleteKey(service: service, account:account)
22
        var dictionary = keyQuery(service: service, account: account)
23
        dictionary[kSecValueRef as String] = key
24
        status = SecItemAdd(dictionary as CFDictionary, nil);
25
    }
26
    return status == errSecSuccess
27
}
28
29
@discardableResult class func deleteKey(service: String, account: String) -> Bool
30
{
31
    var status : OSStatus = -1
32
    if !(service.isEmpty) && !(account.isEmpty)
33
    {
34
        let dictionary = keyQuery(service: service, account: account)
35
        status = SecItemDelete(dictionary as CFDictionary);
36
    }
37
    return status == errSecSuccess
38
}
39
40
class func key(service: String, account: String) -> SecKey?
41
{
42
    var item: CFTypeRef?
43
    if !(service.isEmpty) && !(account.isEmpty)
44
    {
45
        var dictionary = keyQuery(service: service, account: account)
46
        dictionary[kSecReturnRef as String] = kCFBooleanTrue
47
        dictionary[kSecMatchLimit as String] = kSecMatchLimitOne
48
        SecItemCopyMatching(dictionary as CFDictionary, &item);
49
    }
50
    return item as! SecKey?
51
}

Application Passwords

Items secured with the kSecAttrAccessibleWhenUnlocked flag are only unlocked when the device is unlocked, but it relies on the user having a passcode or Touch ID set up in the first place. 

The applicationPassword credential allows items in the keychain to be secured using an additional password. This way, if the user does not have a passcode or Touch ID set up, the items will still be secure, and it adds an extra layer of security if they do have a passcode set.  

As an example scenario, after your app authenticates with your server, your server could return the password over HTTPS that is required to unlock the keychain item. This is the preferred way of supplying that additional password. Hardcoding a password in the binary is not recommended.

Another scenario might be to retrieve the additional password from a user-provided password in your app; however, this requires more work to secure properly (using PBKDF2). We will look at securing user-provided passwords in the next tutorial. 

Another use of an application password is for storing a sensitive key—for example, one that you would not want to be exposed just because the user had not yet set up a passcode. 

applicationPassword is only available on iOS 9 and above, so you will need a fallback that doesn't use applicationPassword if you are targeting lower iOS versions. To use the code, you will need to add the following into your bridging header:

1
#import <LocalAuthentication/LocalAuthentication.h>

2
#import <Security/SecAccessControl.h>

The following code sets a password for the query Dictionary.

1
if #available(iOS 9.0, *)
2
{
3
    //Use this in place of kSecAttrAccessible for the query

4
    var error: Unmanaged<CFError>?
5
    let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlocked, SecAccessControlCreateFlags.applicationPassword, &error)
6
    if accessControl != nil
7
    {
8
        dictionary[kSecAttrAccessControl as String] = accessControl
9
    }
10
    
11
    let localAuthenticationContext = LAContext.init()
12
    let theApplicationPassword = "passwordFromServer".data(using:String.Encoding.utf8)!
13
    localAuthenticationContext.setCredential(theApplicationPassword, type: LACredentialType.applicationPassword)
14
    dictionary[kSecUseAuthenticationContext as String] = localAuthenticationContext
15
}

Notice that we set kSecAttrAccessControl on the Dictionary. This is used in place of kSecAttrAccessible, which was previously set in our passwordQuery method. If you try to use both, you'll get an OSStatus -50 error.

User Authentication

Starting in iOS 8, you can store data in the keychain that can only be accessed after the user successfully authenticates on the device with Touch ID or a passcode. When it's time for the user to authenticate, Touch ID will take priority if it is set up, otherwise the passcode screen is presented. Saving to the keychain will not require the user to authenticate, but retrieving the data will. 

You can set a keychain item to require user authentication by providing an access control object set to .userPresence. If no passcode is set up then any keychain requests with .userPresence will fail. 

1
if #available(iOS 8.0, *)
2
{
3
    let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .userPresence, nil)
4
    if accessControl != nil
5
    {
6
        dictionary[kSecAttrAccessControl as String] = accessControl
7
    }
8
} 

This feature is good when you want to make sure that your app is being used by the right person. For example, it would be important for the user to authenticate before being able to log in to a banking app. This will protect users who have left their device unlocked, so that the banking cannot be accessed. 

Also, if you do not have a server-side component to your app, you can use this feature to perform device-side authentication instead.

For the load query, you can provide a description of why the user needs to authenticate.

1
dictionary[kSecUseOperationPrompt as String] = "Authenticate to retrieve x"

When retrieving the data with SecItemCopyMatching(), the function will show the authentication UI and wait for the user to use Touch ID or enter the passcode. Since SecItemCopyMatching() will block until the user has finished authenticating, you will need to call the function from a background thread in order to allow the main UI thread to stay responsive.

1
DispatchQueue.global().async 
2
{
3
    status = SecItemCopyMatching(dictionary as CFDictionary, &passwordData)
4
    if status == errSecSuccess
5
    {
6
        if let retrievedData = passwordData as? Data
7
        {
8
            DispatchQueue.main.async 
9
            {
10
                //... do the rest of the work back on the main thread

11
            }   
12
        }
13
    }
14
}

Again, we are setting kSecAttrAccessControl on the query Dictionary. You will need to remove kSecAttrAccessible, which was previously set in our passwordQuery method. Using both at once will result in an OSStatus -50 error.

Conclusion

In this article, you've had a tour of the Keychain Services API. Along with the Data Protection API that we saw in the previous post, use of this library is part of the best practices for securing data. 

However, if the user does not have a passcode or Touch ID on the device, there is no encryption for either framework. Because the Keychain Services and Data Protection APIs are commonly used by iOS apps, they are sometimes targeted by attackers, especially on jailbroken devices. If your app does not work with highly sensitive information, this may be an acceptable risk. While iOS is constantly updating the security of the frameworks, we are still at the mercy of the user updating the OS, using a strong passcode, and not jailbreaking their device. 

The keychain is meant for smaller pieces of data, and you may have a larger amount of data to secure that is independent of the device authentication. While iOS updates add some great new features such as the application password, you may still need to support lower iOS versions and still have strong security. For some of these reasons, you may instead want to encrypt the data yourself. 

The final article in this series covers encrypting the data yourself using AES encryption, and while it's a more advanced approach, this allows you to have full control over how and when your data is encrypted.

So stay tuned. And in the meantime, check out some of our other posts on iOS app development!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.