In part 1 we stored small secrets in the Keychain. But most of an app’s data isn’t small secrets, it’s files. A SQLite database with the user’s history, downloaded images, a JSON with the profile, a PDF the user opened. All of that lives on disk, and a lot of people assume iOS encrypts it all the same way. It doesn’t.
What iOS encrypts, and when
iOS encrypts the device storage. That’s true, and it’s always on as long as there’s a passcode. But “the disk is encrypted” doesn’t mean “your files are inaccessible”. The question that matters is a different one: when can the system decrypt a file?
That’s where Data Protection comes in. Every file has a protection class that decides when its encryption key is available. A file can be encrypted on disk and still be readable the moment the device boots, or only when the user unlocks it, depending on the class you assign.
That distinction is what separates “encrypted” from “protected”. Disk encryption defends you if someone pulls the memory chip. Data Protection defends you in the realistic case: a locked device, powered on, in the hands of someone who shouldn’t be reading it.
The protection classes
There are four, from strictest to loosest:
- Complete (
.complete): the file is only readable while the device is unlocked. As soon as it locks, the key is dropped from memory and the file is inaccessible until the next unlock. - CompleteUnlessOpen (
.completeUnlessOpen): like the previous one, but if the file was already open when the device locked, it stays accessible. Useful for a download that continues in the background. - CompleteUntilFirstUserAuthentication (
.completeUntilFirstUserAuthentication): the file is readable from the first unlock after boot, and stays readable even if the device locks again. This is the default on iOS. - None (
.none): the file is readable whenever the device is on, even before the first unlock. No real protection beyond disk encryption.
The default, CompleteUntilFirstUserAuthentication, is a reasonable compromise: it protects against a cold attack (a powered-off device that gets turned on), but not against a locked device that’s been on all day. For genuinely sensitive data, that default isn’t enough.
Raising a file’s protection
When you write data, you can ask for the strictest class directly:
import Foundation
func saveSensitive(_ data: Data, to url: URL) throws {
try data.write(to: url, options: [.completeFileProtection])
}
.completeFileProtection maps to the Complete class: that file will be unreadable while the device is locked. If your app later tries to read it in the background with the screen off, the read fails, and that’s exactly what you want for data that shouldn’t be touched without the user present.
For a file that already exists, or to pick an intermediate class, you adjust it through FileManager attributes:
func protectFile(at url: URL) throws {
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.complete],
ofItemAtPath: url.path
)
}
FileProtectionType has a case for each class: .complete, .completeUnlessOpen, .completeUntilFirstUserAuthentication and .none. Choosing is a matter of answering when you need to read the file. If you only touch it with the app in the foreground, .complete. If you need to read it in the background with the screen locked, you have no choice but to drop to .completeUntilFirstUserAuthentication, and then it’s worth asking whether that data should be on disk at all.
The database case
This is where the theory gets uncomfortable. If you store data in SQLite, Core Data or SwiftData, the database file inherits a protection class, and by default it’s usually the middle compromise. A database file marked accessible after first unlock is readable all day even while the device is locked.
For Core Data you can set the protection when you configure the store:
let description = NSPersistentStoreDescription(url: storeURL)
description.setOption(
FileProtectionType.complete as NSObject,
forKey: NSPersistentStoreFileProtectionKey
)
With .complete, the whole database is protected when the device locks. The cost is real: if the app does background work that touches the database with the screen locked, that work fails. There’s no class that gives you “protected when locked” and “accessible in the background” at once, because those are contradictory goals. Choosing is part of the job.
Where leaks tend to happen
Three places where data escapes the protection you thought you set.
The first is the temporary folder. Files you write to tmp/ or to caches can end up with minimal protection. If you download a sensitive attachment to a temporary file to display it, that temporary can be more readable than the original.
The second is thumbnails and previews. If your app generates a thumbnail of a sensitive document and caches it unprotected, the original document’s protection is worthless: the insecure copy is already on disk.
The third is anything that leaves the app. A file shared through the share sheet, exported to Files, or copied to the clipboard, stops being under your control and under your protection class. Protection only covers what lives in your container.
What we’ve got
In short:
- Disk encryption is always on, but it isn’t the same as protecting your files from access while the device is locked.
- The default (
CompleteUntilFirstUserAuthentication) leaves data readable all day after the first unlock. - For sensitive data, write with
.completeFileProtectionor set.completeon your database store. - Watch the leaks: temporaries, thumbnail caches, and anything you export outside the container.
What’s next
So far we protected data the user generates that lives on their device. But there’s another kind of secret many devs put in the app without thinking: API keys. And there the problem is different, because it doesn’t matter how much you encrypt something that ships inside the binary: if it runs on the user’s device, it can be extracted.
In part 3 we look at why no secret embedded in the app is safe, what to do with the third-party keys you genuinely have to ship, and where a secret that must stay secret actually belongs.
Sources: Encrypting your app’s files (Apple) · FileProtectionType (Apple)