How do you store sensitive data securely on iOS? Explain the Keychain and Data Protection.
MediumAsked oftenSecurityPersistence 2 min read
TL;DR
- Keychain: an encrypted database for small secrets (tokens, passwords, keys). Choose an accessibility value that says when the item can be read.
- Data Protection: file encryption tied to the device passcode. Choose a protection class per file.
- For the highest security, keep keys in the Secure Enclave and require Face ID / Touch ID with access control.
- Never store secrets in
UserDefaults, plain files,Info.plistor source code.
Saving to the Keychain
import Security
enum KeychainError: Error { case status(OSStatus) }
struct Keychain {
static func save(_ value: Data, for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: key
]
SecItemDelete(query as CFDictionary) // replace any old value
var attributes = query
attributes[kSecValueData as String] = value
attributes[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
let status = SecItemAdd(attributes as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.status(status) }
}
static func read(_ key: String) throws -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess else { throw KeychainError.status(status) }
return result as? Data
}
}Four functions do everything: SecItemAdd, SecItemCopyMatching, SecItemUpdate, SecItemDelete.
Accessibility values
| Value | Readable when | Use for |
|---|---|---|
kSecAttrAccessibleWhenUnlocked | Device is unlocked | Default. Most secrets. |
kSecAttrAccessibleAfterFirstUnlock | After the first unlock since reboot | Tokens needed by background tasks |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly | Unlocked, and only if a passcode is set | Very sensitive data |
The ThisDeviceOnly versions are not moved to a new device through backups. Use them for device-bound tokens.
Keychain facts interviewers like
- Items usually survive app deletion. Clear old items on the first launch after install if needed (use a
UserDefaultsflag). - Share items between your apps and extensions with Keychain Access Groups.
kSecAttrSynchronizablesyncs items through iCloud Keychain.
Biometrics with access control
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet, // invalid if Face ID / Touch ID enrollment changes
nil
)!
attributes[kSecAttrAccessControl as String] = accessNow reading the item shows the Face ID prompt. The check is done by the system, not by your code.
A plain LAContext.evaluatePolicy returning true is weaker. On a jailbroken device that check can be patched. Tying the secret to the Keychain is safer.
Data Protection for files
try data.write(to: url, options: .completeFileProtection)
// or on an existing file
try FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.complete],
ofItemAtPath: url.path
)| Class | File readable |
|---|---|
.complete | Only while unlocked |
.completeUnlessOpen | New files can be written while locked. Open files stay readable. |
.completeUntilFirstUserAuthentication | After first unlock (the default) |
.none | Always |
Use .complete for sensitive files. But remember: background tasks cannot read them while the device is locked.
You can set a default for the whole app with the Data Protection capability.
Secure Enclave and CryptoKit
import CryptoKit
let key = try SecureEnclave.P256.Signing.PrivateKey()
let signature = try key.signature(for: Data("challenge".utf8))
// The private key never leaves the Secure Enclave.Store key.dataRepresentation (an encrypted blob) in the Keychain to use the key again later.
Other good practices
- Do not log tokens or personal data.
- Hide sensitive screens in the app switcher snapshot when the app resigns active.
- Use
.ephemeralURL sessions for very sensitive requests. - Remember: anything in the app binary (API keys) can be extracted. Keep real secrets on the server.
Common follow-up questions
- Is the Keychain safe on a jailbroken device? Safer than files, but not perfect. Use
ThisDeviceOnly, biometrics and server-side checks. - Why is my Keychain read failing in the background? The item uses
WhenUnlockedand the device is locked. UseAfterFirstUnlockfor background needs. - Is there a nicer API? Many teams wrap the C API, or use a small library like KeychainAccess.
- What error means "duplicate item"?
errSecDuplicateItem(-25299). Update the item or delete it first.