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
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.
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.
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.
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.
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.
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 - 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.
1---2name: hunting-ios-keychain-and-data-protection-gaps3description: 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.4license: MIT5---67# Hunting iOS keychain and data protection gaps: when a secret is readable while the device is locked89iOS gives every stored secret a protection class that decides when and where it can be read: a keychain item10has an accessibility attribute, and a file has a data-protection class, and together they say whether the11value is available only after the user unlocks the device, whether it is bound to this device alone, and12whether it leaves in a backup. Those classes are safe only when each secret carries the strictest one it can13tolerate. The gap is a secret stored under a class weaker than it needs: a token marked always-accessible that14can be read while the device is locked, a credential not restricted to this device that migrates in a backup15to another one, or a file whose protection class lets it be read outside an unlocked session. The bug is not16that the secret is stored; it is a protection class that exposes it in a context, locked, backed up, migrated,17or post-theft, where it should be unreadable. You hunt these by listing every stored secret and checking the18protection class against the context it must survive.1920## When to use2122- An iOS app stores credentials, tokens, or sensitive values in the keychain or in files.23- A stored item may carry an accessibility or data-protection class weaker than its sensitivity requires.24- Secrets may survive into a device backup or migrate to another device when they should not.2526## Scope check2728Hunt at-rest protection gaps only on apps and devices you own or are authorized to assess, on test devices29and accounts, proving a secret is recoverable in the weaker context without using it against a real account. A30recovered secret authenticates as its owner, so keep every probe within scope and treat recovered material as31sensitive. If you can't name the authorization, stop.3233## The loop34351. **Establish the protection class each secret actually carries and the context it must survive first.** For36 each stored secret, determine its keychain accessibility or file data-protection class and the strongest37 context it needs to resist: locked-device read, backup inclusion, migration to another device, or38 post-theft access. This is the false-positive killer: a secret stored with the strictest class it can39 tolerate, available only when unlocked and bound to this device, is not exposed even though it exists on40 disk. Name the class and the context before claiming a gap.41422. **Enumerate stored secrets and their storage.** List the credentials, tokens, keys, and sensitive values43 the app stores, and for each whether it is in the keychain or a file, and the accessibility or44 data-protection class assigned. Secrets whose class is looser than the strictest they can tolerate are the45 candidates.46473. **Check the locked-device availability.** Determine whether a secret marked available while the device is48 locked, or with a file class readable outside an unlocked session, can be read without the user unlocking.49 A secret needed only while the app is in active use should not be available when the device is locked.50514. **Check the this-device and backup restrictions.** Determine whether a secret lacks the this-device-only52 restriction, so it migrates to another device through a backup or transfer, and whether it is included in53 backups at all. A secret bound to the hardware should never appear on another device.54555. **Check the post-theft and other-app reach.** Determine whether the weaker class lets the secret be read by56 an attacker with physical possession of a locked device, or through a backup an attacker can obtain, or by57 another app sharing an access group it should not. The context that matters is the one the attacker58 actually has.59606. **Confirm and record.** Confirm by recovering a usable secret in the weaker context on a test device, read61 while locked, extracted from a backup, or present after migration, without using it against a real account.62 Kill the lead if every secret carries the strictest class it can tolerate, if none is available while63 locked or in a backup beyond its need, and if none migrates off the device. Record the secret, its64 protection class, the context it was recovered in, and the proof, or set a `kill_reason`.6566## Where at-rest protection leaks6768- **The protection class is the finding.** A stored secret is expected; the gap is a class weaker than the69 context requires, exposing it while locked, in a backup, or after migration. Name the class and the context.70- **Always-accessible items read while locked.** A keychain item marked available regardless of lock state, or71 a file class readable outside an unlocked session, is exposed on a locked device.72- **Missing this-device-only migrates secrets.** A secret without the this-device restriction travels in a73 backup or device transfer to hardware the owner did not intend, defeating device binding.74- **Backups carry secrets off the device.** A secret included in a backup is exposed wherever the backup can75 be read, which may be far weaker than the device's own protection.76- **Shared access groups widen reach.** A keychain access group broader than needed lets another app read a77 secret that should be private to the storing app.7879## Worked example (a confirm and a kill)8081> **Confirm.** An app stores a long-lived authentication token in the keychain marked available regardless of82> lock state and without the this-device-only restriction. The token is read from a locked test device and is83> also present after restoring a backup to a second device, recovered without using it against a real account.84> **Confirmed** at-rest secret exposure through a weak keychain accessibility class, `high`, remediation = store85> the token with an accessibility class available only when the device is unlocked and restricted to this86> device, exclude it from backups, and scope the keychain access group to the app alone.87>88> **Kill.** The same token is stored with an accessibility class available only after first unlock while89> unlocked, restricted to this device, excluded from backups, and in an access group private to the app.90> It cannot be read on a locked device, does not appear after restoring a backup to another device, and no91> other app can reach it. **Killed**, `kill_reason` = "the token carries the strictest class it can tolerate,92> unlocked-only and this-device-only, is excluded from backups, and is privately scoped; no read in a weaker93> context recovers it."9495## Rationalizations to reject9697- *"The keychain is encrypted."* -> Encryption is governed by the accessibility class; an always-accessible98 item is decryptable while the device is locked, so the class, not the fact of encryption, decides exposure.99- *"It is only on the device."* -> Without the this-device-only restriction the secret migrates through a100 backup or transfer to another device; confirm the binding rather than assuming local means local.101- *"Backups are the user's own."* -> A backup can be read on a computer or in a cloud store far weaker than102 the device; exclude sensitive secrets from backups rather than trusting the backup's protection.103- *"Another app cannot read our keychain."* -> Only if the access group is scoped to the app; a broad or104 shared access group lets a sibling app read the item.105- *"The user has a passcode."* -> A passcode gates unlocked access, but an always-accessible item or a106 backup-included secret is reachable without unlocking or off the device entirely.107108## Executing this in practice109110You need every stored secret with its keychain accessibility or file data-protection class, its this-device111and backup status, and its access group, plus the strictest context each secret must survive. For each,112decide whether the class exposes it while locked, in a backup, after migration, or to another app. Reading the113assigned classes settles most leads; recovering a usable secret in the weaker context on a test device, while114locked, from a backup, or after migration, settles the rest.115116## Related117118- `hunting-mobile-secret-and-storage-exposure` - the cross-platform secret-at-rest hunt, of which the iOS119 keychain and data-protection classes here are the platform-specific mechanism.120- `auditing-mobile-biometric-and-local-auth-bypass` - a keychain item gated by biometric presence relies on121 the local-authentication check that skill audits, so protection class and auth gate meet on the same item.122- `auditing-ios-app-group-and-pasteboard-exposure` - a secret shared through an app group or the pasteboard123 leaves the keychain's protection, the adjacent exposure that skill covers.124- `auditing-mobile-root-jailbreak-and-tamper-resistance` - a jailbroken device weakens the platform guarantees125 these classes rely on, the environment that skill audits.126- [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the secret stored under a weak protection class, sink127 = the read outside the intended unlocked and on-device context, evidence = a usable secret recovered while128 locked, from a backup, or after migration on a test device.