It’s true that Apple cares very much about user’s privacy. Since iOS 8.3, it’s impossible to access arbitrary data in an application’s sandbox. Additionally, developers may choose to enable “Data Protection” option for their app, which will encrypt the sandbox if the device is locked with a passcode. Nonetheless, some extra protection wouldn’t hurt, if it didn’t involve too much work.
Core Data is a popular choice for implementing data persistence in iOS and OS X applications. Typically, it utilises embedded SQLite database. One of the things we could try is to encrypt it. It may seem like an overkill on iOS, since application’s sandbox is not accessible by third parties. But still, the device can be jailbroken, the OS can have unpublished security flaws, or maybe we might want to backup the database somewhere in the cloud. In such cases, the extra protection might become useful. And of course, there is OS X, where application’s data is exposed.
If you want to improve your app’s security, and you also use Core Data, you may try encrypted-core-data, open source library that replaces default database storage. Encrypted Core Data internally uses SQLCipher, which provides transparent encryption of the whole database.
How to integrate
Setting up an encrypted database is quite simple. It involves adding the library to the project, and changing few values during Core Data setup.
- Add „EncryptedCoreData” to your project via CocoaPods:
pod 'EncryptedCoreData'
- Find addPersistentStoreWithType call in your code. Add passphrase to options (ideally stored in the keychain, or obtained from backend via secure connection), and change store type to EncryptedStoreType:
let options = [EncryptedStorePassphraseKey : "key"] try coordinator.addPersistentStoreWithType(EncryptedStoreType, configuration: nil, URL: url, options: options)
At this point, the app is configured to use encrypted sqlite, with the important caveat that migration from standard to encrypted (and vice-versa) is not supported. Encrypted-core-data uses different naming scheme for rows and tables, so it’s not as simple as encrypting an existing database. In this case, the only possible option is either to perform migration manually, or not at all (if app has not been released yet).
Performance
Understandably, encrypting data on the fly will consume more CPU time, making Core Data related operations slower. This also means larger energy consumption and so on. To test that, I created few benchmarks that would perform exactly the same operations on standard and encrypted storage, and compare the results.
Note about the tests
- All measurements are in milliseconds.
- Tests were performed twice, with a smaller database (charts on the left, approximately 8000 objects), and a larger one (charts on the right, approx. 32000 objects).
- On charts, the shorter bar is better (operation executed faster).
- The scheme used for this tests is a simple model of a blog, where users can publish posts and comments.
- Migration involved deleting one class, adding another, and adding and removing properties.
- Tests from charts were performed on iPhone 6 and iPhone 5s, both with iOS 9.
- Most recent encrypted-core-data from a master branch was used.
Creating database objects
In this test, a large number of objects has been inserted into the database, then modified, and then deleted (saving between each action).
Small count – iPhone 6 | Large count – iPhone 6 |
Small count – iPhone 5s | Large count – iPhone 5s |
Encrypted Core Data performed significantly worse, especially with smaller datasets. It might not be a problem if your app doesn’t modify database too much, or only few objects change simultaneously. Otherwise, the waiting time may be quite noticeable for the user, especially if modifications are preceded by requests to the backend.
Fetching objects
Another test involved performing queries on the database with varying levels or complexity, and count of fetched objects.
Name | Predicate | Count(small) | Count(large) |
---|---|---|---|
long query, small result | content like 'test'
AND author.name contains 'john2'
AND date >= %@
AND author.email contains[cd] '@' | 118 | 778 |
long query, big result | content like 'test'
AND author.name contains 'john'
AND date >= %@
AND author.email contains[cd] '@' | 2018 | 16005 |
count query | comments.@count >= %@ | 20 | 20 |
combined AND comparision | date >= %@
AND author.name contains 'john' | 2018 | 16006 |
date comparision | date >= %@ | 2018 | 16006 |
text comparision | author.name like 'john4' | 200 | 1600 |
fetch all | TRUEPREDICATE | 4000 | 3200 |
Here are the results:
Small count – iPhone 6 | Large count – iPhone 6 |
Small count – iPhone 5s | Large count – iPhone 5s |
Overall, encrypted-core-data seemed to perform pretty well, in some cases better than Apple’s storage. One surprise was the result for fetch request containing count (i.e. fetch all users who wrote more than 5 comments). It seems that SQLCipher is poorly optimised for this task, perhaps decrypting more data than necessary. On the other hand, a long query with a small result and text comparison took longer on the original SQLite store.
There is another problem, not seen on the chart: NSSQLiteStore fetches all scalar properties of the fetched objects, while encrypted-core-data only their identifiers. This leads to huge performance penalty when reading fetched objects. Here is an example:
let result = try! stack.context.executeFetchRequest(request) as! [Comment]
for comment in result {
_ = comment.content
}
Fetching objects and reading property
Small count – iPhone 6 | Large count – iPhone 6 |
Small count – iPhone 5s | Large count – iPhone 5s |
For encrypted storage, there is an extra SQL query for every accessed object. Therefore, the more objects are fetched and read, the more time it takes. This is especially apparent for a query fetching all objects from the database. At least data is not fetched again if we want to check another property on the same object.
Aggregate functions
Small count – iPhone 6 | Large count – iPhone 6 |
Small count – iPhone 5s | Large count – iPhone 5s |
A single value (like average, count, etc.) was calculated from all existing objects. Results are comparable. Surprisingly, original SQLite was slightly faster than EncryptedStore on iPhone 5s, but slightly slower on iPhone 6.
Migration
Small count – iPhone 6 | Large count – iPhone 6 |
Small count – iPhone 5s | Large count – iPhone 5s |
Finally, I measured the time it takes to perform lightweight, automatic migration. Encrypted Core Data performed pretty well for a small data set, but so-so for a bigger one.
Unfortunately, a more complicated migration (like using a custom mapping file) is currently not supported.
Missing features & bugs
During tests, I noticed that quite a few things are not supported, or don’t work properly:
- Subquery support
- Customised migration (only automatic migration works)
- External storage for large binary files
- Write-Ahead Logging
- (#240) string comparison operators
- (#241) some aggregate functions – average, sum, etc.
- query with nested count, i.e comments.@count > 5 works, but author.comments.@count > 5 doesn’t
Tools
Below, a few tools for managing SQLCipher databases, which support Encrypted Core Data just fine:
DB Browser for SQLite. Open source database manager, gives the possibility to look up tables, stored data. Entering password is required to inspect encrypted databases. SQLCipher support is available as a separate binary download.
SQLiteManager. A similar, but proprietary tool. Promoted by the creators of SQLCipher.
Summary
Overall, it’s hard to say if Encrypted Core Data is a good choice for your app. Using it would definitely require some sacrifices, regarding user’s experience, or development speed.
While performance problems may be concerning, missing features aren’t that critical. I encourage you to try the library and maybe submit Pull Request, for stuff that you need.