hunting-ios-keychain-and-data-protection-gaps · git:20260908.e3ae18a · 2026-09-08 · sha256 99948695bbbe7b1b
hunting-ios-keychain-and-data-protection-gaps git:20260908.e3ae18aB
Immutable. This exact content is served forever at /api/v1/blob/99948695bbbe7b1b.
--- name: hunting-ios-keychain-and-data-protection-gaps description: >- Hunt at-rest exposure of secrets on iOS, where a credential, token, or sensitive value is stored with a keychain accessibility class or a data-protection class weaker than it needs, so it is readable when the device is locked, survives into a device backup, or is reachable by another app or after theft, because the item is marked always-accessible, is not restricted to this device, or the file protection lets it be read outside an unlocked session. Use when reviewing how an app stores secrets and files and which protection class each carries. Covers over-permissive keychain accessibility, missing this-device-only restriction, backup-included secrets, and weak file data-protection classes. The secret stored under a weak protection class is the source, the read outside the intended unlocked and on-device context is the sink, and recovering a usable secret in that weaker context is the bug. license: MIT --- # Hunting iOS keychain and data protection gaps: when a secret is readable while the device is locked iOS gives every stored secret a protection class that decides when and where it can be read: a keychain item has an accessibility attribute, and a file has a data-protection class, and together they say whether the value is available only after the user unlocks the device, whether it is bound to this device alone, and whether it leaves in a backup. Those classes are safe only when each secret carries the strictest one it can tolerate. The gap is a secret stored under a class weaker than it needs: a token marked always-accessible that can be read while the device is locked, a credential not restricted to this device that migrates in a backup to another one, or a file whose protection class lets it be read outside an unlocked session. The bug is not that the secret is stored; it is a protection class that exposes it in a context, locked, backed up, migrated, or post-theft, where it should be unreadable. You hunt these by listing every stored secret and checking the protection class against the context it must survive. ## When to use - An iOS app stores credentials, tokens, or sensitive values in the keychain or in files. - A stored item may carry an accessibility or data-protection class weaker than its sensitivity requires. - Secrets may survive into a device backup or migrate to another device when they should not. ## Scope check Hunt at-rest protection gaps only on apps and devices you own or are authorized to assess, on test devices and accounts, proving a secret is recoverable in the weaker context without using it against a real account. A recovered secret authenticates as its owner, so keep every probe within scope and treat recovered material as sensitive. If you can't name the authorization, stop. ## The loop 1. **Establish the protection class each secret actually carries and the context it must survive first.** For each stored secret, determine its keychain accessibility or file data-protection class and the strongest context it needs to resist: locked-device read, backup inclusion, migration to another device, or post-theft access. This is the false-positive killer: a secret stored with the strictest class it can tolerate, available only when unlocked and bound to this device, is not exposed even though it exists on disk. Name the class and the context before claiming a gap. 2. **Enumerate stored secrets and their storage.** List the credentials, tokens, keys, and sensitive values the app stores, and for each whether it is in the keychain or a file, and the accessibility or data-protection class assigned. Secrets whose class is looser than the strictest they can tolerate are the candidates. 3. **Check the locked-device availability.** Determine whether a secret marked available while the device is locked, or with a file class readable outside an unlocked session, can be read without the user unlocking. A secret needed only while the app is in active use should not be available when the device is locked. 4. **Check the this-device and backup restrictions.** Determine whether a secret lacks the this-device-only restriction, so it migrates to another device through a backup or transfer, and whether it is included in backups at all. A secret bound to the hardware should never appear on another device. 5. **Check the post-theft and other-app reach.** Determine whether the weaker class lets the secret be read by an attacker with physical possession of a locked device, or through a backup an attacker can obtain, or by another app sharing an access group it should not. The context that matters is the one the attacker actually has. 6. **Confirm and record.** Confirm by recovering a usable secret in the weaker context on a test device, read while locked, extracted from a backup, or present after migration, without using it against a real account. Kill the lead if every secret carries the strictest class it can tolerate, if none is available while locked or in a backup beyond its need, and if none migrates off the device. Record the secret, its protection class, the context it was recovered in, and the proof, or set a `kill_reason`. ## Where at-rest protection leaks - **The protection class is the finding.** A stored secret is expected; the gap is a class weaker than the context requires, exposing it while locked, in a backup, or after migration. Name the class and the context. - **Always-accessible items read while locked.** A keychain item marked available regardless of lock state, or a file class readable outside an unlocked session, is exposed on a locked device. - **Missing this-device-only migrates secrets.** A secret without the this-device restriction travels in a backup or device transfer to hardware the owner did not intend, defeating device binding. - **Backups carry secrets off the device.** A secret included in a backup is exposed wherever the backup can be read, which may be far weaker than the device's own protection. - **Shared access groups widen reach.** A keychain access group broader than needed lets another app read a secret that should be private to the storing app. ## Worked example (a confirm and a kill) > **Confirm.** An app stores a long-lived authentication token in the keychain marked available regardless of > lock state and without the this-device-only restriction. The token is read from a locked test device and is > also present after restoring a backup to a second device, recovered without using it against a real account. > **Confirmed** at-rest secret exposure through a weak keychain accessibility class, `high`, remediation = store > the token with an accessibility class available only when the device is unlocked and restricted to this > device, exclude it from backups, and scope the keychain access group to the app alone. > > **Kill.** The same token is stored with an accessibility class available only after first unlock while > unlocked, restricted to this device, excluded from backups, and in an access group private to the app. > It cannot be read on a locked device, does not appear after restoring a backup to another device, and no > other app can reach it. **Killed**, `kill_reason` = "the token carries the strictest class it can tolerate, > unlocked-only and this-device-only, is excluded from backups, and is privately scoped; no read in a weaker > context recovers it." ## Rationalizations to reject - *"The keychain is encrypted."* -> Encryption is governed by the accessibility class; an always-accessible item is decryptable while the device is locked, so the class, not the fact of encryption, decides exposure. - *"It is only on the device."* -> Without the this-device-only restriction the secret migrates through a backup or transfer to another device; confirm the binding rather than assuming local means local. - *"Backups are the user's own."* -> A backup can be read on a computer or in a cloud store far weaker than the device; exclude sensitive secrets from backups rather than trusting the backup's protection. - *"Another app cannot read our keychain."* -> Only if the access group is scoped to the app; a broad or shared access group lets a sibling app read the item. - *"The user has a passcode."* -> A passcode gates unlocked access, but an always-accessible item or a backup-included secret is reachable without unlocking or off the device entirely. ## Executing this in practice You need every stored secret with its keychain accessibility or file data-protection class, its this-device and backup status, and its access group, plus the strictest context each secret must survive. For each, decide whether the class exposes it while locked, in a backup, after migration, or to another app. Reading the assigned classes settles most leads; recovering a usable secret in the weaker context on a test device, while locked, from a backup, or after migration, settles the rest. ## Related - `hunting-mobile-secret-and-storage-exposure` - the cross-platform secret-at-rest hunt, of which the iOS keychain and data-protection classes here are the platform-specific mechanism. - `auditing-mobile-biometric-and-local-auth-bypass` - a keychain item gated by biometric presence relies on the local-authentication check that skill audits, so protection class and auth gate meet on the same item. - `auditing-ios-app-group-and-pasteboard-exposure` - a secret shared through an app group or the pasteboard leaves the keychain's protection, the adjacent exposure that skill covers. - `auditing-mobile-root-jailbreak-and-tamper-resistance` - a jailbroken device weakens the platform guarantees these classes rely on, the environment that skill audits. - [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the secret stored under a weak protection class, sink = the read outside the intended unlocked and on-device context, evidence = a usable secret recovered while locked, from a backup, or after migration on a test device.