How to Use iOS Data Protection
Modern iOS devices support data protection, which secures user data with built-in encryption hardware. Here, we’ll look at information from a few sources, as well as my own observations, to see how apps can use this to protect their files.
Every file stored on an iOS device has one of the four data protection types, which determine when the file can be read from and written to. These protection types can be set either with an entitlement or programmatically. You should aim to protect users’ data as much as possible, so from best to worst, these types are:
-
NSFileProtectionComplete
— The file is only accessible while the device is unlocked. When the device is locked, your app will receive aUIApplicationProtectedDataWillBecomeUnavailable
notification, and it’ll lose access to protected files 10 seconds later. Use this if possible, but not if your app needs to access the file while it’s running in the background. -
NSFileProtectionCompleteUnlessOpen
— While the device is locked, files can still be created, and files that are already open can be accessed. Use this for completing a task in the background that requires you to save new data or update a database. -
NSFileProtectionCompleteUntilFirstUserAuthentication
— The file can be accessed any time after the user enters their passcode once after the device has booted, even when the device is locked. Use this if your app needs to read the file while it’s running in the background. -
NSFileProtectionNone
— The file can always be accessed. The system needs to use this in some places, but your app probably doesn’t.
If you try to access a file when it’s protected, the operation will fail with either NSFileReadNoPermissionError
(code 257) or NSFileWriteNoPermissionError
(code 513).
You can read more about each protection type in Apple’s platform security documentation.
Note that data protection doesn’t work with iOS Simulator. FileVault encryption used on Macs works very differently. Even setting a protection type and then reading it back won’t work. You’ll always read NSURLFileProtectionCompleteUntilFirstUserAuthentication
— at least on my Mac. Always test data protection on a real device.
Setting a Default Protection Level
In this section, we’ll see how you can set the default protection level for files created in your app or in your extension’s sandbox container.
CompleteUntilFirstUserAuthentication
You don’t need to do anything because this has been the default since iOS 7.
Complete
The default protection level for files created in your app or extension’s container is determined by the com.apple.developer.default-data-protection
entitlement, which appears as Data Protection when editing the entitlements file in Xcode. You need to set this for the app and all extensions. There are two caveats to bear in mind:
-
This entitlement needs to be set before the app is installed, as Quinn “The Eskimo!” says on the Apple developer forums:
The default data protection set in your entitlements (and hence from your App ID via your provisioning profile) is only applied when your app container is created.
In my investigation, changing the entitlement for an already installed app sometimes resulted in the new level being used for new files, and sometimes it didn’t. It definitely won’t apply to existing files, so unless you’re making a brand-new app, you’ll need to set protection types programmatically. This is described later in this post.
-
Since the entitlement only applies to your app or extension’s container, you need to set the protection level for a shared container programmatically.
With these caveats out of the way, setting the entitlement is easy. In your Xcode project settings, go to the Capabilities tab for your app or extension and turn on Data Protection. That’s it.
This will add a data protection entitlement to your app’s entitlements file (creating it if it doesn’t exist) with the entitlement’s value set to NSFileProtectionComplete
. It’ll also enable data protection on your App ID in the Certificates, Identifiers & Profiles section of the Apple developer website.
See the Add a capability to a target section of Xcode Help for more details.
CompleteUnlessOpen
You need to follow the steps for NSFileProtectionComplete
— described above along with the caveats — and then change the protection level in both the entitlements file and on your App ID in the Certificates, Identifiers & Profiles section of the Apple developer website. These two values must be manually kept in sync; otherwise, you’ll see an error about invalid entitlements when building your project.
The place to navigate to on the website is: Identifiers > App IDs > (the app ID) > Edit > Data Protection > Sharing and Permissions. This can be set to one of the three protection levels (but not NSFileProtectionNone
).
The key expected in the entitlements file is NSFileProtectionCompleteUnlessOpen
.
I found that even with automatic provisioning, Xcode didn’t update the provisioning profile for the changes made on the website. You can force Xcode to download the profile again by deleting it from ~/Library/MobileDevice/Provisioning Profiles/
, and by identifying the correct one using Craig Hockenberry’s Provisioning QuickLook plugin.
None
You can’t make this the default because there’s no option for this in the Certificates, Identifiers & Profiles section of the Apple developer website. You wouldn’t want that anyway.
Setting a Protection Level Programmatically
If your app is already installed on users’ devices without the entitlement set and you want something other than NSFileProtectionCompleteUntilFirstUserAuthentication
, you’ll need to set the protection type programmatically, both to protect new files and to upgrade existing ones. Note that you don’t need to set the entitlement to use these APIs.
Setting a Protection Level for a Particular File
Let’s first look at single files; there are several APIs that accept a protection type as an option.
If you’re writing a new file from Data
/NSData
, use:
try data.write(to: fileURL, options: .completeFileProtection)
For an existing file, you can use either NSFileManager
/FileManager
or NSURL
:
try FileManager.default.setAttributes([.protectionKey: FileProtectionType.complete], ofItemAtPath: fileURL.path) // Or // cast as `NSURL` because the `fileProtection` property of `URLResourceValues` is read-only. try (fileURL as NSURL).setResourceValue(URLFileProtection.complete, forKey: .fileProtectionKey)
With Core Data, you can pass the protection type when adding the persistent store:
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: [NSPersistentStoreFileProtectionKey: FileProtectionType.complete])
Setting a Protection Level for New Files in a Directory
Quinn “The Eskimo!” also posted this useful information in the Apple developer forums:
By default the data protection value is inherited from the parent directory when you create an item. For example, if you have a directory set to
NSFileProtectionComplete
, any items created within that directory will, by default, be set toNSFileProtectionComplete
. The entitlement controls the data protection value for the root directory of your container, which is then inherited by anything created within that container. However, if you explicitly set the value for a directory then subsequent items created within that directory will get the new value by default.
Therefore, you can use the FileManager
/NSFileManager
or NSURL
API as above with a directory URL instead of a file URL, or pass the protection attribute when creating the directory:
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: [.protectionKey: FileProtectionType.complete])
You can then create files in the directory as normal.
⚠️ Update: On iOS 14 and later, ensure you don’t use the .atomic
option when creating a file if you want that file to inherit the protection level of the containing directory. Atomic writing is implemented by first writing the file to a temporary location and then moving that temporary file to the final location. iOS 14 changed the temporary location from being alongside the final location to being in some other temporary directory. This means the file inherits the protection level of the temporary directory, and moving a file doesn’t change that file’s protection level. In turn, the file may not end up with the protection level you want.
Changing the Protection Level for All Existing Files in a Directory
When you set the protection level of a directory, the protection level of existing files in that directory doesn’t change; you’ll need to set the protection level for every file in the directory. This can be done with FileManager.DirectoryEnumerator
/NSDirectoryEnumerator
:
guard let directoryEnumerator = FileManager.default.enumerator(at: directoryURL, includingPropertiesForKeys: [], options: [], errorHandler: { url, error -> Bool in print(error) return true }) else { print("Could not create directory enumerator at \(directoryURL.path)") return } // `NSEnumerator` is not generic in Swift so we have to deal with `Any`. for urlAsAny in directoryEnumerator { do { try (urlAsAny as! NSURL).setResourceValue(URLFileProtection.complete, forKey: .fileProtectionKey) } catch { print(error) } }
Conclusion
In this post, we covered how to leverage data protection on modern iOS devices to protect user data with built-in encryption hardware. We learned about the four data protection classes, in addition to when each one is appropriate. Then, we saw the code required to protect new or existing files using one of these protection classes. Since very little code is required, it seems well worth the time to add this additional layer of security to keep our users’ files private.