Keychain: Secure Storage for Secrets

Learn to store and retrieve secrets — passwords, credentials, and tokens — safely using Keychain Services, including the SecItemAdd/SecItemCopyMatching/SecItemUpdate/SecItemDelete pattern, access control, and accessibility levels.

The previous lesson ended with a firm rule: secrets do not belong in UserDefaults. This lesson covers the tool that exists specifically to fill that gap. Keychain Services is a C-based API, older and noticeably less friendly than NSUserDefaults, built around dictionaries of attributes rather than simple setter and getter methods. That friction is not an accident — Keychain is handling data where mistakes have real security consequences, and its API reflects the seriousness of that job. By the end of this lesson you will be able to store a token securely, retrieve it, update it, and delete it, and you will understand the accessibility and access-control options that let you tune exactly when and how a stored secret can be read.

What belongs in Keychain

Keychain is the correct home for anything where a leak would meaningfully harm the user: passwords, API keys, OAuth access and refresh tokens, biometric-protected data, encryption keys, and similar credentials. If a piece of data would let someone impersonate the user, access their account, or read data they should not be able to read, it belongs in Keychain — full stop. This is a narrower category than 'anything private'; ordinary user content like notes or photos is private but not this specific kind of secret, and typically belongs in the file system or a database rather than Keychain, which is not designed or sized for bulk content storage.

The distinguishing feature that separates Keychain from everything else in this course's persistence lessons is that Keychain items are encrypted at rest by the operating system. A UserDefaults plist sitting on disk can be read in plain text by anyone with file-system access; a Keychain item cannot be read the same way, because the OS itself is responsible for encrypting and decrypting it, gated behind the accessibility and access-control settings you configure when you store it.

The Keychain Services C API pattern

Keychain Services is a lower-level C API than most of what Objective-C developers deal with day to day, and its four core functions follow a consistent, dictionary-based pattern: SecItemAdd to insert a new item, SecItemCopyMatching to search for and retrieve one, SecItemUpdate to modify an existing item's data, and SecItemDelete to remove one. In every case, you describe what you are looking for, adding, or changing using an NSDictionary (bridged to a CFDictionaryRef) whose keys are Keychain attribute constants — kSecClass to say what kind of item this is, kSecAttrAccount to name it, kSecValueData to hold the actual secret bytes, and several others for narrower matching or configuration.

This dictionary-of-attributes design is deliberately more verbose than NSUserDefaults' simple key-value calls, but it buys you precision: you can search Keychain by any combination of attributes, restrict what kind of item you are dealing with, and attach access-control policies directly to the query. Once you have written this pattern once, storing and retrieving any secret in your app follows essentially the same shape every time.

Storing and retrieving a token: a worked example

The most common real-world use of Keychain in an Objective-C app is storing an authentication token after a successful login, and reading it back on subsequent launches so the user does not have to log in again every time. Storing it means building a query dictionary describing the item — its class (kSecClassGenericPassword is the usual choice for arbitrary secret data like a token), an account identifier so you can find it again later, and the actual token data — then passing that dictionary to SecItemAdd.

Retrieving it means building a similar query dictionary identifying the same item by its class and account, but this time adding kSecReturnData to ask for the actual secret bytes back, and passing it to SecItemCopyMatching, which returns the data through an out-parameter rather than as a normal return value, in keeping with the C API conventions Keychain Services follows throughout.

Updating an existing item uses SecItemUpdate, which takes a query dictionary identifying the item to change, plus a second dictionary of attributes to update — this matters because calling SecItemAdd a second time for an item that already exists returns an error rather than silently overwriting it. Deleting an item, for example on logout, uses SecItemDelete with a query dictionary identifying the item to remove.

Access control: gating access behind biometrics or a passcode

Beyond simply encrypting data at rest, Keychain supports attaching an access-control policy to an item through the kSecAttrAccessControl attribute, built using the SecAccessControlCreateWithFlags function. This lets you require an additional authentication step — Face ID, Touch ID, or the device passcode — at the moment an app tries to read the item, on top of the OS-level encryption that already protects it.

This is appropriate for especially sensitive items where you want an extra confirmation step even from within your own authenticated app — for instance, gating access to a stored payment credential or an encryption key behind Face ID specifically, so that even a compromised or automated part of your own app's code cannot silently read the secret without the user physically present to authenticate. This is a more advanced, situational feature; most everyday tokens and credentials do not need it, but it is worth knowing it exists for the cases that do.

Accessibility levels: controlling when an item can be read

Separately from access control, every Keychain item has an accessibility level, set via the kSecAttrAccessible attribute, which governs the conditions under which the item is available to be read at all — most notably, in relation to whether the device is locked or unlocked, and whether the item is allowed to migrate to a new device through a backup restore.

kSecAttrAccessibleWhenUnlocked, one of the most commonly used values, means the item can only be read while the device is unlocked, and is a sensible default for most credentials — the app cannot read it in the background while the device is locked, which limits exposure if the device is compromised while locked. Other accessible constants offer variations on this: some allow access even shortly after the device locks (useful for background tasks that need brief continued access), and some are scoped 'ThisDeviceOnly', meaning the item is excluded from backups and will not migrate to a new device even if the user restores from an iCloud or iTunes backup — appropriate for a secret that should never leave the specific device it was created on, such as a device-bound encryption key.

The tradeoff across these constants is essentially a spectrum between convenience and exposure: a more permissive accessibility level (readable even when the device is locked, migrating freely with backups) is more convenient for background operations and restoring the user's data to a new device, while a stricter level (readable only when unlocked, confined to this device) minimizes the window in which the secret could be exposed. You do not need to memorize every constant Apple offers; what matters is understanding this tradeoff conceptually so you can choose sensibly for whatever secret you are storing.

Secure storage guarantees, concretely

It is worth being explicit about what 'encrypted at rest' actually buys you, since it is easy to treat as a vague marketing phrase rather than a concrete guarantee. On modern Apple platforms, Keychain items are encrypted using keys tied to the device's hardware security, and in many cases further protected by the device passcode itself, meaning that even someone with direct access to the device's storage cannot simply read Keychain contents in plain text the way they could read an unprotected plist file. This is precisely the guarantee UserDefaults cannot offer, and precisely why the two exist as separate tools rather than one API trying to do both jobs.

This guarantee is not unconditional, however — it depends on choosing an appropriate accessibility level (an item set to be readable at all times, even when the device is locked, has a larger exposure window than one restricted to unlocked-only access) and, for especially sensitive items, layering access control on top. Keychain gives you the mechanism; using it well still requires making sensible choices about accessibility and, where warranted, access control, for each specific secret you are storing.

Device and platform considerations

Keychain interacts with backups, iCloud Keychain syncing, and device migration in ways worth understanding at a conceptual level, even without needing to master every detail. By default, many Keychain items are included in encrypted device backups and can be restored onto a new device when the user sets it up from a backup, which is generally what users expect — they do not want to re-enter every password and re-authenticate every account after getting a new phone. iCloud Keychain syncing goes further, actively synchronizing certain Keychain items across a user's own devices that share the same Apple ID, again driven by user expectation that their saved credentials follow them across their devices.

Both of these conveniences are things you actively opt out of, not into, using the ThisDeviceOnly family of accessibility constants when an item genuinely should never leave the specific device it was created on. The right default for most app-level credentials — session tokens, saved login credentials — is to allow normal backup and restore behavior, since that matches what users expect from every other app on the platform; the ThisDeviceOnly restriction is reserved for narrower cases, such as a hardware-bound encryption key that would be meaningless or actively wrong to restore onto different hardware.

The rule restated: never UserDefaults, always Keychain, for secrets

It is worth closing this lesson exactly where the spec closes this module, because it is the single most important takeaway across both this lesson and the one before it: never store passwords, tokens, or other long-lived secrets in NSUserDefaults. UserDefaults is not merely 'less secure' than Keychain in some abstract, minor sense — its backing plist file is not encrypted at rest at all, which makes it a genuinely inappropriate, unsafe home for exactly the category of data Keychain exists to protect.

Keychain is the correct and, in this ecosystem, the only appropriate tool for that category of data. Every other storage mechanism covered across this course's persistence lessons — property lists, raw files, SQLite, Core Data — shares UserDefaults' lack of built-in encryption at rest for the data it holds. When you are deciding where a piece of data belongs and that data is a password, a token, a key, or any other credential, the decision is not really a judgment call at all: it goes in Keychain.

The four core Keychain Services functions

Every Keychain function returns an OSStatus; errSecSuccess (0) means the call succeeded, and any other value is an error code worth checking explicitly. Toll-free bridging (__bridge) is required to pass an NSDictionary where a CFDictionaryRef is expected, since Keychain Services is a plain C API.

OSStatus SecItemAdd(CFDictionaryRef query, CFTypeRef *result);
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result);
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
OSStatus SecItemDelete(CFDictionaryRef query);

// Common attribute keys used inside the query dictionaries:
// kSecClass, kSecClassGenericPassword, kSecAttrAccount,
// kSecValueData, kSecReturnData, kSecAttrAccessible,
// kSecAttrAccessControl

Code Examples

This builds a query dictionary describing a generic-password item — the usual class for arbitrary secret data like a token — identified by an account name, holding the token as raw bytes, and restricted to being readable only while the device is unlocked. SecItemAdd inserts it, and the OSStatus return value tells us whether it succeeded.

OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);

kSecReturnData:@YES tells Keychain we want the actual secret bytes back, not just a confirmation the item exists. The result comes back through the CFTypeRef out-parameter, which we bridge to NSData and decode back into a string. Checking status against errSecSuccess before touching result guards against the case where no matching item was found. __bridge_transfer hands ownership of the returned Core Foundation object over to ARC, so it is released automatically once no longer referenced — a detail specific to bridging C API return values into Objective-C memory management.

SecItemUpdate takes two dictionaries: one identifying which item to change, and a second holding only the attributes to change — here, just the new token bytes. SecItemDelete needs only the identifying query, since it removes rather than modifies. Calling SecItemAdd again on an account that already has a stored item, instead of SecItemUpdate, would fail with a duplicate-item error, which is why login flows typically check for an existing item and call update rather than add on subsequent logins.

Common mistake: Calling SecItemAdd a second time to 'overwrite' an existing token instead of using SecItemUpdate.

- (BOOL)saveToken:(NSString *)token forAccount:(NSString *)account {
    NSData *tokenData = [token dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecAttrAccount: account,
        (__bridge id)kSecValueData: tokenData,
        (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked
    };
    OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
    return status == errSecSuccess;
}
- (nullable NSString *)tokenForAccount:(NSString *)account {
    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecAttrAccount: account,
        (__bridge id)kSecReturnData: @YES,
        (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne
    };
    CFTypeRef result = NULL;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
    if (status != errSecSuccess || result == NULL) {
        return nil;
    }
    NSData *tokenData = (__bridge_transfer NSData *)result;
    return [[NSString alloc] initWithData:tokenData encoding:NSUTF8StringEncoding];
}
- (BOOL)updateToken:(NSString *)newToken forAccount:(NSString *)account {
    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecAttrAccount: account
    };
    NSDictionary *update = @{
        (__bridge id)kSecValueData: [newToken dataUsingEncoding:NSUTF8StringEncoding]
    };
    OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query,
                                     (__bridge CFDictionaryRef)update);
    return status == errSecSuccess;
}

- (BOOL)deleteTokenForAccount:(NSString *)account {
    NSDictionary *query = @{
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecAttrAccount: account
    };
    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
    return status == errSecSuccess;
}

Keychain accessibility: convenience vs. exposure

Choosing an accessibility level is a tradeoff between convenience (background access, migration to new devices) and minimizing the window a secret is exposed.

more convenient, larger exposure window
  <----------------------------------------------------
  Always accessible   WhenUnlocked   WhenUnlocked, ThisDeviceOnly
  (readable even       (readable       (readable only unlocked,
   while locked)       while unlocked)  never leaves this device)
  ---------------------------------------------------->
  more restrictive, smaller exposure window

Important Rules

Anything that would let someone impersonate the user or access their account — passwords, tokens, API keys — goes in Keychain, never UserDefaults.

Use SecItemAdd to insert a new item, SecItemUpdate to change an existing one, never SecItemAdd twice for the same identity.

Use SecItemCopyMatching with kSecReturnData to retrieve an item's actual secret bytes, and check the OSStatus before trusting the result.

Choose an accessibility level deliberately: kSecAttrAccessibleWhenUnlocked is a sensible default; ThisDeviceOnly variants are for secrets that must never migrate to another device.

Reserve kSecAttrAccessControl (biometric/passcode gating) for especially sensitive items that warrant an extra authentication step even within an already-authenticated app.

Never store passwords or other long-lived secrets in NSUserDefaults — Keychain is the correct and only appropriate tool for that category of data.

Login flows are the canonical Keychain use case

Almost every app with a login screen follows the same Keychain pattern this lesson demonstrates: after a successful authentication call to a backend, the resulting session or refresh token is saved via SecItemAdd (or SecItemUpdate on a re-login), so the next time the app launches it can call SecItemCopyMatching, find the stored token, and silently re-authenticate the user without asking them to type their password again. On logout, SecItemDelete removes the item so a subsequent login starts clean. This exact four-function pattern — add, retrieve, update, delete — appears in some form in nearly every production Objective-C or Swift app that has user accounts, which is why it is worth knowing cold rather than looking up from scratch each time.

Calling SecItemAdd a second time to update an already-stored token.

Use SecItemUpdate for an item that already exists; reserve SecItemAdd for genuinely new items.

SecItemAdd fails with a duplicate-item error if an item matching the same identifying attributes already exists, so blindly calling it again on every login silently fails to save the new token.

Storing a device-bound encryption key with a default accessibility level that allows it to migrate freely to a new device via backup restore.

Use a ThisDeviceOnly accessibility constant for secrets that are only meaningful on the specific device where they were created.

A key tied to specific hardware becomes meaningless, or actively incorrect, if restored onto different hardware — ThisDeviceOnly accessibility prevents that migration from happening in the first place.

Ignoring the OSStatus return value from SecItemAdd, SecItemCopyMatching, SecItemUpdate, or SecItemDelete.

Always check the returned OSStatus against errSecSuccess before assuming the operation worked.

Keychain Services is a C API that communicates failure through return codes rather than exceptions; silently ignoring a non-success status means your app can proceed as if a save or read succeeded when it actually failed.

Falling back to NSUserDefaults for a password or token 'just temporarily' or 'because Keychain feels like too much code for this one case'.

Use Keychain for every credential, with no exceptions, regardless of how small or 'temporary' the use case feels.

This is the rule the spec insists on with the most force: UserDefaults' plist storage is never encrypted at rest, so there is no scenario, however small, where substituting it for Keychain is acceptable for a real secret.

Which Keychain Services function is used to insert a brand-new item that does not already exist? A. SecItemUpdate B. SecItemCopyMatching C. SecItemAdd D. SecItemDelete

C. SecItemAdd — SecItemAdd inserts a new item. Calling it again for an item that already exists (matching the same identifying attributes) fails with a duplicate-item error rather than overwriting it.

What does kSecReturnData:@YES accomplish in a SecItemCopyMatching query? A. It deletes the matched item after returning it B. It requests that the actual secret bytes be returned, not just confirmation the item exists C. It forces the item to sync via iCloud Keychain D. It encrypts the returned data a second time

B. It requests that the actual secret bytes be returned, not just confirmation the item exists — Without kSecReturnData, SecItemCopyMatching can confirm an item's existence but will not hand back its actual stored value; setting it to YES is what causes the secret bytes to come back through the result out-parameter.

Why is kSecAttrAccessibleWhenUnlocked a commonly sensible default accessibility level for a stored credential? A. It makes the item readable at all times, even while the device is locked B. It restricts the item to being readable only while the device is unlocked, limiting exposure without being overly restrictive C. It disables encryption for faster access D. It is required for all Keychain items and cannot be changed

B. It restricts the item to being readable only while the device is unlocked, limiting exposure without being overly restrictive — WhenUnlocked accessibility balances usability and security: the app can read the item during normal use, but it cannot be read while the device sits locked, reducing the window of potential exposure.

When should you reach for a ThisDeviceOnly accessibility constant? A. For every single Keychain item, as a matter of course B. Never — ThisDeviceOnly items cannot be encrypted C. For a secret, such as a device-bound encryption key, that should never migrate to another device via backup or restore D. Only for items that also use kSecAttrAccessControl

C. For a secret, such as a device-bound encryption key, that should never migrate to another device via backup or restore — ThisDeviceOnly variants exclude an item from backup and restore migration, which is appropriate specifically for secrets that are meaningless or incorrect on any device other than the one that created them.

What guarantee does Keychain provide that NSUserDefaults does not? A. Faster read and write performance for large datasets B. Automatic support for complex queries and relationships C. Encryption at rest, managed by the operating system D. The ability to store custom Objective-C objects without any serialization

C. Encryption at rest, managed by the operating system — The defining difference this lesson and the previous one both emphasize is that Keychain items are encrypted at rest by the OS, while UserDefaults' backing plist file is not — this is precisely why secrets belong in Keychain and never in UserDefaults.

What is the correct and only appropriate place, in this ecosystem, for a long-lived password or authentication token? A. NSUserDefaults, as long as registerDefaults: was used first B. A property list file stored in the app's Documents directory C. Keychain D. Core Data, since it is encrypted by default

C. Keychain — This is the rule stated explicitly across both this lesson and the previous one: Keychain is the correct and only appropriate tool for passwords and other long-lived secrets — none of the other storage mechanisms in this course provide the same encryption-at-rest guarantee.

Sketch, in words or pseudocode, the Keychain calls a login screen should make in each of these three situations: (1) the user logs in for the first time on this device, (2) the user's token needs to be refreshed after it expires but the account stays the same, and (3) the user taps 'log out'.

(1) First login: no existing item for this account, so call SecItemAdd with the new token. (2) Token refresh: an item already exists for this account, so call SecItemUpdate with the new token data rather than SecItemAdd. (3) Log out: call SecItemDelete for that account's item so no stale token remains. SecItemCopyMatching is used separately, at launch, to check whether a valid stored token already exists before deciding whether to show the login screen at all.

Match each situation to exactly one of the four Keychain functions covered in this lesson: SecItemAdd, SecItemUpdate, SecItemCopyMatching, or SecItemDelete.