BeatChain: How to write a testable Keychain wrapper

Tsolis Anastasios
Beat Engineering Blog
5 min readNov 13, 2019

--

Set the stage

At Beat, we value the privacy of our users. For that reason, we have chosen to protect their sensitive information securely using Keychain Services. Interacting with the Keychain is quite confusing, as you have to use C based APIs. As a result, the common approach is to use a wrapper to make it easier to deal with.

Although there are many good 3rd party Keychain wrappers out there, we needed one that would be testable, with a simple API that would fit our needs and ideally be implemented in Swift. For these reasons, we decided to build and open source our own wrapper, called BeatChain!

In this article, the Keychain Services will be presented at a high level and the architectural approach of how to build a testable Keychain wrapper will be described.

Keychain Services Overview

According to the Keychain documentation:

When you want to store sensitive data such as a password or a cryptographic key, you package it as a Keychain item. Along with the data itself, you provide a set of publicly visible attributes both to control the item’s accessibility and to make it searchable. As shown in Figure 1, Keychain services handle data encryption and storage (including data attributes) in a Keychain, which is an encrypted database stored on disk. Later, authorized processes use Keychain services to find the item and decrypt its data.

Figure 1: Storing data and attributes into a Keychain.

Each Keychain item is represented as a dictionary and its attributes are set as keys and values. kSecClass attribute specifies the class of a Keychain item and based on it, special attributes are supported.

In order to perform CRUD operations in Keychain, Security Framework provides 4 main methods:

Modelling Keychain

Now that we have described Keychain Services at a high level, let’s dive into code!

Initially, we create a wrapper for the 4 main API methods, called Keychain. Now, each time we want to perform CRUD operations, we will use the Keychain class and not the functions of Security Framework.

KeychainResult is a model that contains two properties: The status code and the result object of the fetch action.

We have created the Keychain class to be able to write unit tests for the objects that they want to interact with the Keychain database (more on this below). The only thing missing to mock Keychain CRUD operations is to create a protocol that requires the above functions and makes the Keychain class conform to it. Let’s name this the KeychainProtocol.

Modelling Keychain Items

As already mentioned, each Keychain item is represented as a dictionary and its arguments are set as its keys and values. For each item we want to store, we can create a struct that will be initialized with some attributes and then, through a computed property called query, we can build the final dictionary that represents it.
For example, for storing the user’s pin code, we have created a GenericPasswordItem struct which is initialized with a service and an optional account and access group. Then, the query property creates a dictionary that models this item.

Because every item must be able to create a dictionary representation of it, we create a Queryable protocol that requires a query property. Now, every Keychain item should conform to this protocol and provide an appropriate implementation.

By representing Keychain items in this way, we can test that its dictionary depiction is correct.

Modelling logic related to Keychain interaction

Till now, we save only String values in Keychain, such as the user’s pin. Each time we read/save/delete a String value we perform specific steps, including:

  • To read a value, we add some more attributes to the item’s dictionary representation related to reading operation, we read the item, we perform error checking based on OSStatus response and finally, if no errors occur, we decode the query result to a String Value.
  • To delete a value, we delete the item and then we perform error checking based on OSStatus response.
  • To save a value, we add some attributes to the item’s dictionary representation related to saving operation, we update the item if it exists or we create a new item if it doesn’t. Finally, we perform error checks based on OSStatus.

Due to the fact that we repeat these steps for each String value, our goal is to create a simplified API, isolate this logic in one place, deal with our own kind of errors, instead of OSStatus and test all this logic. To achieve our goals, we create a new class called KeychainManager.

As a result, all the logic that we examined before is isolated in one class and we can write tests about it.

An important thing to notice is that the KeychainManager is initialized with an instance that conforms to KeychainProtocol.

So, in our code the KeychainManager can be instantiated with the Keychain class, but in our tests we can initialize it with a MockKeychain that conforms to that protocol. Having now a MockKeychain instance in the KeychainManager we can observe all the arguments that are passed to it and return whatever results we want, according to each test case.

Now, let’s assume that we want to test saving a new value to an already saved item. Then we can write a test for this.

Or let’s say that we want to delete an item that does not exist.

As a final step, due to the fact that this KeychainManager can be used by other objects, e.g. an object responsible to migrate a String value from UserDefaults to the Keychain, the KeychainManager should be able to be mocked when you are going to write tests for these objects. For this reason, we create a protocol named KeychainManagerProtocol which requires the interface of KeychainManager and then we make the KeychainManager conform to it.

Conclusion

Building a keychain wrapper that fits your needs is not so hard after all. Avoiding third party libraries and using objects as complex as our needs is a practice that worked for us and, thus, we highly recommend it.

If we have to save items of type other than String in the future, we will refactor the KeychainManager, having testability in mind, in order to meet future needs.

Credits

In order to build the keychain wrapper, we were deeply influenced by GenericKeychain. Also, useful ideas has been taken from Keychain Services API Tutorial for Passwords in Swift.

Interested in BeatChain?

Feel free to contribute, share your thoughts or try it out on your application! Discover more on GitHub: github.com/beatlabs/BeatChain

Want to get a taste of our in-house developed tools and how we use them to build the future of urban mobility? Check all our openings.

Anastasios Tsolis is a Senior iOS Engineer in the Mobile Infra Team at Beat. He is passionate about iOS and quality code. A constant learner who loves solving complex architectural problems having always testability in mind.

--

--