iOS Data Protection and Keychain Security

iOS Data Protection and Keychain Security - article cover image Mobile API OSINT
Time it takes to read this article 5 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

iOS protects data at rest through two complementary mechanisms. Data Protection encrypts files in the app’s sandbox with per-file keys that are themselves wrapped by class keys derived from the device UID and the user’s passcode, so files can be made unreadable while the device is locked. The Keychain is a separate, independently protected database for small, high-value secrets — passwords, tokens, certificates — that persists across app reinstalls and offers finer-grained accessibility control than ordinary files. Together they are the two primary tools an iOS developer has for keeping secrets safe from a device that is lost, stolen, or backed up somewhere the developer does not control.

Both mechanisms are opt-in in the sense that the developer chooses a protection/accessibility class for every file and every Keychain item, and the default choices are not always the strictest ones. An app that stores an OAuth token in NSUserDefaults, or that creates a Keychain item with an overly-permissive accessibility class, has effectively opted out of most of the protection iOS is capable of providing — the data is technically “on an encrypted filesystem” but readable the moment the device is unlocked once, or even while locked, depending on the class chosen.

Attack Prerequisites

Extracting or abusing weakly protected iOS data generally requires:

  • Physical access to the device (lost/stolen scenario) or an unencrypted, unpassworded iTunes/Finder backup obtained from a synced computer.
  • For live inspection during an authorized assessment, a jailbroken device or a resigned build with Frida/objection injected via frida-gadget, since iOS sandboxing normally prevents one app from reading another’s container.
  • An app that uses a weak Data Protection class on sensitive files, or a permissive Keychain accessibility attribute, or stores secrets outside either mechanism entirely (plist, NSUserDefaults, SQLite/Core Data unencrypted).

How It Works

Every file protected by Data Protection has a randomly generated per-file key wrapped by one of several class keys (NSFileProtectionComplete, CompleteUnlessOpen, CompleteUntilFirstUserAuthentication, None). The class keys are themselves protected by keys derived from the device’s hardware UID (burned into the Secure Enclave, never leaves it) mixed with the user’s passcode. NSFileProtectionComplete keys are only available while the device is unlocked and are discarded from memory shortly after lock; CompleteUntilFirstUserAuthentication (the default for most third-party app data) keeps the class key available from first unlock after boot until the next reboot; NSFileProtectionNone uses a key protected only by the hardware UID and is available even before the passcode is entered.

The Keychain is a SQLite database managed by securityd/securid, separate from the app sandbox, with each item carrying its own kSecAttrAccessible* constant that governs both *when* it can be read and *whether* it migrates to a new device via backup or device-transfer. ...WhenUnlocked and ...AfterFirstUnlock mirror the file protection classes; appending ThisDeviceOnly prevents the item from being included in an encrypted backup or restored onto different hardware, which matters for keys that should never leave the Secure Enclave’s device binding. The deprecated kSecAttrAccessibleAlways — still accepted by the API for compatibility — makes an item readable even before the passcode is ever entered after boot, which for most secrets defeats the purpose of using Keychain at all.

Neither mechanism protects against an app simply choosing not to use it: NSUserDefaults (backed by a plist under the app’s Library/Preferences) and unencrypted SQLite/Core Data stores are ordinary sandboxed files with whatever default file protection class Xcode’s project settings assign — commonly CompleteUntilFirstUserAuthentication, not Complete — and none of the Keychain’s accessibility semantics apply to them.

Vulnerable Code / Configuration

A Keychain item created with the always-available accessibility class, storing a long-lived refresh token that should require the device to have been unlocked at least once since boot, at minimum:

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "refresh_token",
    kSecValueData as String: tokenData,
    // VULNERABLE: readable even before first unlock after boot,
    // and included verbatim in unencrypted-style migrations.
    kSecAttrAccessible as String: kSecAttrAccessibleAlways
]
SecItemAdd(query as CFDictionary, nil)
SWIFT

Worse, and common in practice: bypassing Keychain entirely and persisting the same token to NSUserDefaults, a plaintext plist with no per-item access control at all:

// VULNERABLE: auth token in a plist, not the Keychain.
// Readable by any code running as this app, and often swept
// into device/iCloud backups unless the whole domain is excluded.
UserDefaults.standard.set(refreshToken, forKey: "authToken")
SWIFT

Walkthrough / Exploitation

On a jailbroken device (or a resigned build with Frida injected), enumerate the Keychain and app sandbox with objection:

objection -g com.target.app explore
com.target.app on (iPhone: 16.x) [usb] # ios keychain dump
com.target.app on (iPhone: 16.x) [usb] # ios nsuserdefaults get
Bash

Inspect the sandbox directly for plaintext secrets outside the Keychain — plists, SQLite databases, and cache files are all fair game:

com.target.app on (iPhone: 16.x) [usb] # env
# ... shows Documents/, Library/Preferences/, Library/Caches/ paths
plutil -p Library/Preferences/com.target.app.plist
sqlite3 Documents/app.sqlite ".dump" | grep -i token
Bash

For a non-jailbroken assessment, an unencrypted local backup pulled via idevicebackup2 (part of libimobiledevice) exposes any Keychain items whose accessibility class did not include ThisDeviceOnly, plus the full app container files, without needing a jailbreak at all — which is why ThisDeviceOnly matters even for otherwise well-classified secrets:

idevicebackup2 backup --full ./backup-dir
# then parse Manifest.db / the backup's Keychain plist for exported items
Bash

Note: The Simulator’s Keychain is not encrypted the same way as a physical device’s — do not draw security conclusions from Simulator testing. Always validate accessibility-class behavior on real hardware, and remember kSecAttrAccessibleAlways/kSecAttrAccessibleAlwaysThisDeviceOnly are deprecated precisely because they undermine the passcode-gated model.

Opsec: When testing your own app’s protection classes, force-lock the device (Cmd+lock or the side button) and attempt to read the target file/item from a background process — Data Protection differences only manifest while the device is actually locked, not merely while the app is backgrounded.

Detection and Defense

Correct defaults get most apps most of the way there; the remaining work is choosing the tightest class each secret can tolerate:

  • Store all secrets in the Keychain, never in NSUserDefaults, plists, or unencrypted Core Data/SQLite fields.
  • Use kSecAttrAccessibleWhenUnlockedThisDeviceOnly (or AfterFirstUnlockThisDeviceOnly only when background access is genuinely required) for anything sensitive, and avoid the deprecated ...Always classes entirely.
  • Set NSFileProtectionComplete on sensitive files created directly in the sandbox, and verify Xcode’s default protection level matches expectations.
  • Back high-value keys with the Secure Enclave and an access-control object (SecAccessControlCreateWithFlags with .biometryCurrentSet or .userPresence) so use requires a fresh biometric/passcode check.
  • Exclude sensitive on-disk caches from backup with URLResourceValues.isExcludedFromBackup when a value must never leave the device via iCloud/iTunes.

Real-World Impact

Mobile forensic suites used by both incident responders and, in unauthorized contexts, attackers with physical device access, routinely target exactly this gap — Keychain items and app files left at weak or default protection classes are a standard extraction target once a device is unlocked or a backup is obtained, which is why MASVS/MASTG storage testing treats Keychain accessibility-class review as a baseline check on every iOS assessment.

Conclusion

iOS gives developers strong, hardware-rooted primitives for protecting data at rest, but both Data Protection and Keychain are only as strong as the class the developer selects — and the weakest failure mode, storing secrets outside either mechanism, opts out of protection altogether. Default to the Keychain with the tightest ThisDeviceOnly class each secret can tolerate, and treat any use of NSUserDefaults or a bare plist for anything sensitive as a finding.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL