
You've probably picked a keychain accessibility constant, shipped your app, and moved on. Most developers do. But that choice quietly controls when iOS will and won't hand your app its own secrets, and the wrong pick doesn't throw an error; it just fails silently at the worst possible moment. What happens next depends entirely on which class you actually understand.
When you store a keychain item, the kSecAttrAccessible attribute specifies under which conditions the system decrypts and returns that item. iOS provides several data protection classes:
and their corresponding ThisDeviceOnly variants, which prevent items from being migrated to other devices through backup or sync.
kSecAttrAccessibleAlways allows access to the item regardless of whether the device is locked. This offers convenience but provides the weakest protection, as data remains available even when the device is locked.
kSecAttrAccessibleWhenUnlocked restricts access to periods when the device is actively unlocked by the user. This is generally the preferred option for sensitive data that needs to be available only during active use, as it leverages the device's strongest protection state.
kSecAttrAccessibleAfterFirstUnlock allows access after the user has unlocked the device once following a reboot. Once this first unlock occurs, the item remains accessible even if the device is subsequently locked. This class is often used for background processes that need keychain access when the device is locked but has been unlocked at least once since boot.
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly requires that a device passcode be configured and ties the cryptographic keys to that passcode. It also restricts the item to the originating device. If the passcode is removed, these items become inaccessible. This class is suitable for data that needs stronger guarantees that it can't be restored or used on another device.
In general, ThisDeviceOnly variants improve security by ensuring that keychain items aren't included in backups or iCloud sync, reducing the risk of exposure if backup data is compromised or if data is migrated to another device.
Although the kSecAttrAccessible attribute on a keychain item specifies when decryption is permitted, enforcement of that policy at runtime depends on the device's hardware-based security mechanisms, including the Secure Enclave on supported devices.
Before any of this reaches a device, though, it's worth catching a misconfigured class while the build is still sitting on your machine. Running the compiled binary through an IPA auditor surfaces exactly which items were stored under weak or default kSecAttrAccessible values, well before the app ships and the Secure Enclave becomes the only thing standing between a locked device and an exposed secret.
When an app requests access to a keychain item, the system consults the relevant accessibility and authentication state before allowing decryption of the underlying data.
If the required conditions aren't met, for example, if the device has no passcode set, biometric authentication has failed or not been performed when required, or the device is in a state that doesn't satisfy the protection class, access to the necessary key material is denied.
In this situation, decryption fails even if the app holds the correct entitlements and uses the proper keychain access group.
As a result, keychain protection classes do more than control logical access based on identifiers and entitlements; they're tied to the device's security state.
This ensures that kSecValueData for a keychain item remains inaccessible whenever the configured data protection requirements aren't satisfied, regardless of the app's authorization to query the item.
The Secure Enclave's enforcement of keychain protection classes has a direct impact on whether background tasks can access keychain items.
If a keychain item is stored with kSecAttrAccessibleWhenUnlocked, it's only available while the device is unlocked. As soon as the device locks, background tasks that rely on that item will fail to retrieve it.
Using kSecAttrAccessibleAfterFirstUnlock mitigates this issue for many use cases. Once the user has unlocked the device after a reboot, items in this class remain accessible to background tasks even if the device later locks, until the next reboot.
kSecAttrAccessibleAlways avoids this limitation by allowing access regardless of lock state, but it's deprecated and not recommended for new designs due to security concerns.
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly can support certain background flows, but only when the user has a passcode configured. Additionally, any ThisDeviceOnly variant that depends on the device being in an unlocked state will still block background operations once the device locks.
As a result, for background flows that must operate reliably while the device is locked, kSecAttrAccessibleAfterFirstUnlock (or its ThisDeviceOnly counterpart, with the above caveats) is generally the appropriate choice.
Despite being deprecated, kSecAttrAccessibleAlways still appears in production applications for several related reasons. Legacy keychain items are a primary factor: many apps never update or rewrite existing entries, so any items created with the old accessibility class retain it indefinitely.
In addition, some code paths or libraries may apply it as a default, causing new items to inherit this setting even when the app doesn't explicitly request it.
Development teams may also consider kSecAttrAccessibleAlways convenient because it allows access to keychain items regardless of the device's lock state.
However, this behavior corresponds to the weakest protection level from the perspective of securityd and Secure Enclave enforcement. In scenarios involving a device compromise, such as a successful exploit or jailbreak, the distinction between locked and unlocked states becomes largely irrelevant.
Under those conditions, items stored with the "Always" class are more exposed than those protected by stricter accessibility classes, increasing the risk to sensitive data such as tokens, PINs, and passwords.
Access groups add a layer of control on top of the kSecAttrAccessible data protection classes. Before the system considers an item's data protection class, securityd checks whether the calling app is entitled to use the item's access group. If the app's entitlements don't include that group, SecItemCopyMatching fails immediately, regardless of the kSecAttrAccessible value.
This also means that access groups must be scoped carefully. If multiple apps share the same access group, each of those apps can read and modify items stored under that group, as long as they meet the other Keychain query constraints. No choice of data protection class can prevent this kind of cross-app access once the group is shared.
In practice, both mechanisms are complementary: access groups control which apps are allowed to query a given item, and kSecAttrAccessible controls under what device and lock state conditions the item can be returned.
Misconfiguring either, by omitting required access groups or by using groups that are too broad, undermines the security properties you expect from your Keychain configuration.
Matching a data protection class to the actual use case is important because an inappropriate choice can either expose credentials while the device is locked or cause features to fail at runtime.
Use kSecAttrAccessibleWhenUnlocked for tokens and session keys that are only needed while the app is active in the foreground.
Use kSecAttrAccessibleAfterFirstUnlock when background execution (for example, VoIP or background refresh) must access credentials without requiring the user to unlock the device each time.
Select kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly when you want to enforce that the device has a passcode and you don't need the items to migrate to other devices via backups or syncing, thereby strengthening the security posture.
Avoid the "Always"-type classes unless you have explicitly analyzed and accepted the reduced protection they provide for locked-state access.
If the selected class is too restrictive for your access pattern, you may encounter silent failures, such as missing tokens or authentication errors, so it's advisable to implement retry logic and clear user prompts to recover when access is denied.
Once you have mapped each use case to an appropriate Keychain data protection class, you should verify that these choices are correctly applied at runtime.
Begin by reviewing your entitlements, paying particular attention to keychain-access-groups and com.apple.developer.default-data-protection, to understand the baseline configuration.
Next, examine each SecItemAdd call to ensure that it specifies an explicit kSecAttrAccessible value rather than relying on system defaults, which may not align with your security requirements.
Search the codebase for uses of kSecAttrAccessibleAlways and other permissive accessibility constants that could weaken data protection, and replace them where necessary with more restrictive options that match your threat model.
Conduct controlled tests using SecItemCopyMatching while the device is in both locked and unlocked states to verify the effective accessibility of stored items.
For deeper analysis, you can use dynamic instrumentation tools such as Frida or Objection to hook SecItemAdd and SecItemCopyMatching.
This allows you to observe the actual attributes passed at runtime and confirm that each secret is stored under the intended protection class.
When you call SecItemAdd without specifying kSecAttrAccessible, the keychain applies a default accessibility class that may not align with your app's runtime access patterns.
This can be problematic for background refresh, early-boot operations, or any scenario where the device may still be locked.
In these cases, the default choice can cause SecItemCopyMatching to return no results until the device is unlocked, which can disrupt token-based workflows that depend on immediate keychain access.
If you can assume that a device passcode is always set, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly provides stronger security guarantees by tying the item to the presence of a passcode and to the current device, while offering access timing similar to kSecAttrAccessibleWhenUnlocked.
However, items with this attribute can't be stored on devices without a passcode, and attempts to add them will fail in that environment.
To avoid unexpected behavior and to ensure that stored items are available when needed, explicitly set kSecAttrAccessible to a value that reflects your app's expected access conditions, such as locked vs. unlocked state, device-only scope, and backup/restore requirements.
You've seen how keychain data protection classes quietly control when your secrets are actually accessible, not just whether they're encrypted. Get the class wrong, and you'll break background tasks, weaken backup security, or confuse users with unexpected prompts. Audit what you're already shipping, match each item's class to its real access pattern, and treat the default behavior as something you've consciously chosen, not something you've simply inherited.