iosNudge

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.plist or 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

ValueReadable whenUse for
kSecAttrAccessibleWhenUnlockedDevice is unlockedDefault. Most secrets.
kSecAttrAccessibleAfterFirstUnlockAfter the first unlock since rebootTokens needed by background tasks
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyUnlocked, and only if a passcode is setVery 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 UserDefaults flag).
  • Share items between your apps and extensions with Keychain Access Groups.
  • kSecAttrSynchronizable syncs 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] = access

Now 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
)
ClassFile readable
.completeOnly while unlocked
.completeUnlessOpenNew files can be written while locked. Open files stay readable.
.completeUntilFirstUserAuthenticationAfter first unlock (the default)
.noneAlways

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 .ephemeral URL 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 WhenUnlocked and the device is locked. Use AfterFirstUnlock for 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.