If you need to keep a password, API token or refresh token on an iPhone, the Keychain is the right place. You add the secret with SecItemAdd, read it with SecItemCopyMatching, change it with SecItemUpdate and remove it with SecItemDelete. This guide shows each call in Swift, explains the attributes that matter, and covers the mistakes that cause most Keychain bugs.
What is the iOS Keychain?
Keychain Services is Apple’s encrypted storage for small, sensitive values: passwords, tokens, cryptographic keys and certificates. Items are encrypted by the system, and by default only the app that created an item can read it. Apps from the same developer can share items through an access group.
The Keychain is built for small secrets, not for whole user profiles or large files.
Keychain vs UserDefaults vs files
- Keychain: passwords, auth tokens, encryption keys. Encrypted, with controls over when the data can be read.
- UserDefaults: non-sensitive settings such as theme or onboarding state. Stored unencrypted in a property list.
- Files, SwiftData or Core Data: larger or structured app data. Protect them with file protection, and keep the encryption key in the Keychain if the content is sensitive.
A good rule: if leaking the value would let someone act as the user, it belongs in the Keychain.
How a Keychain query works
Every Keychain call takes a dictionary of attributes. The dictionary says which item you mean, or which item you want to create. For a generic secret, the identity of an item is the combination of kSecClass, kSecAttrService and kSecAttrAccount.
The attributes you will actually use
kSecClass(required): the kind of item. Common values arekSecClassGenericPassword(app secrets and tokens),kSecClassInternetPassword(a credential for a server),kSecClassCertificate,kSecClassKeyandkSecClassIdentity.kSecAttrService: a label for the part of your app that owns the item, such ascom.example.myapp.auth. Use it with generic passwords.kSecAttrAccount: the account or key name, such as a username orrefreshToken.kSecAttrServer: the host name forkSecClassInternetPassworditems. This is the Internet-password counterpart of the service attribute.kSecValueData: the secret itself, asData.kSecAttrAccessible: when the item can be read (see the next section).kSecAttrAccessControl: an access control object, used for biometrics or passcode requirements. Do not set it together withkSecAttrAccessible; the access control object already carries an accessibility value.kSecAttrAccessGroup: the Keychain access group, used to share items between your own apps. It needs the Keychain Sharing capability.kSecAttrSynchronizable: set to true to sync an item through iCloud Keychain.kSecAttrLabel: a human-readable label.kSecReturnData,kSecReturnAttributes,kSecReturnRef: choose what a search returns.kSecAttrCreationDateandkSecAttrModificationDatecome back as attributes; the system sets them, so you do not pass them in.kSecMatchLimit:kSecMatchLimitOnefor a single result orkSecMatchLimitAllfor every match.kSecAttrKeyTypeandkSecAttrKeySizeInBits: used withkSecClassKeywhen creating cryptographic keys, for examplekSecAttrKeyTypeECSECPrimeRandomand 256 bits.
kSecAttrAccess appears in some older tutorials, but it is macOS-only and does not apply to iOS.
Choose when the data can be read: kSecAttrAccessible
This is the attribute most tutorials skip, and it matters. It controls when the system will release the item.
kSecAttrAccessibleWhenUnlocked: readable only while the device is unlocked.kSecAttrAccessibleWhenUnlockedThisDeviceOnly: same, but the item is not included in backups or synced to a new device.kSecAttrAccessibleAfterFirstUnlock: readable after the first unlock following a restart. Useful for background tasks.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly: the same, not migrated to other devices.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly: readable only while unlocked, and only if the device has a passcode. The item is deleted if the passcode is removed.
For most auth tokens, WhenUnlockedThisDeviceOnly is a safe default. Use AfterFirstUnlock only if your app genuinely needs the secret while the device is locked, for example during background refresh. Avoid kSecAttrAccessibleAlways; Apple deprecated it.
The four core functions
SecItemAdd(_:_:): create a new item. It fails witherrSecDuplicateItemif one already exists.SecItemCopyMatching(_:_:): find items and return data or attributes.SecItemUpdate(_:_:): change an existing item. You pass one dictionary that finds the item and another with the new values.SecItemDelete(_:): remove matching items.
All four return an OSStatus. errSecSuccess means success. Related code that is not a failure of your app, such as errSecItemNotFound, is part of normal flow and should be handled.
A complete, reusable Keychain wrapper
The wrapper below handles add, read, update, save-or-update and delete, with typed errors. It uses a generic password item scoped by service and account.
import Foundation
import Security
enum KeychainError: Error {
case duplicateItem
case itemNotFound
case unexpectedData
case unhandled(OSStatus)
}
struct KeychainStore {
let service: String
private func baseQuery(account: String) -> [String: Any] {
[
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
}
/// Creates a new item. Throws .duplicateItem if it already exists.
func add(_ data: Data, account: String) throws {
var query = baseQuery(account: account)
query[kSecValueData as String] = data
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
let status = SecItemAdd(query as CFDictionary, nil)
switch status {
case errSecSuccess:
return
case errSecDuplicateItem:
throw KeychainError.duplicateItem
default:
throw KeychainError.unhandled(status)
}
}
/// Returns the stored data. Throws .itemNotFound if nothing is saved.
func read(account: String) throws -> Data {
var query = baseQuery(account: account)
query[kSecReturnData as String] = kCFBooleanTrue
query[kSecMatchLimit as String] = kSecMatchLimitOne
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status != errSecItemNotFound else { throw KeychainError.itemNotFound }
guard status == errSecSuccess else { throw KeychainError.unhandled(status) }
guard let data = result as? Data else { throw KeychainError.unexpectedData }
return data
}
/// Replaces the data of an existing item.
func update(_ data: Data, account: String) throws {
let attributes: [String: Any] = [kSecValueData as String: data]
let status = SecItemUpdate(baseQuery(account: account) as CFDictionary,
attributes as CFDictionary)
guard status != errSecItemNotFound else { throw KeychainError.itemNotFound }
guard status == errSecSuccess else { throw KeychainError.unhandled(status) }
}
/// Adds the item, or updates it if it already exists.
func save(_ data: Data, account: String) throws {
do {
try add(data, account: account)
} catch KeychainError.duplicateItem {
try update(data, account: account)
}
}
/// Deletes the item. A missing item is not treated as an error.
func delete(account: String) throws {
let status = SecItemDelete(baseQuery(account: account) as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.unhandled(status)
}
}
}
Using the wrapper
let store = KeychainStore(service: "com.example.myapp.auth")
do {
// Save (adds or updates)
try store.save(Data(refreshToken.utf8), account: "refreshToken")
// Read
let data = try store.read(account: "refreshToken")
let token = String(decoding: data, as: UTF8.self)
// Delete on sign-out
try store.delete(account: "refreshToken")
} catch KeychainError.itemNotFound {
// Nothing saved yet: show the sign-in screen
} catch {
print("Keychain error: \(error)")
}
Prefer storing a token over a raw password. If the token leaks, you can revoke it; a leaked password has to be changed everywhere the user reused it.
Turn OSStatus codes into readable messages
A bare number such as -25299 is hard to debug. SecCopyErrorMessageString(_:_:) converts an OSStatus into a readable description. Apple lists the codes in its Security framework result codes.
extension KeychainError: LocalizedError {
var errorDescription: String? {
switch self {
case .duplicateItem: return "An item with this key already exists."
case .itemNotFound: return "No item was found for this key."
case .unexpectedData: return "The stored data was not in the expected format."
case .unhandled(let status):
let message = SecCopyErrorMessageString(status, nil) as String?
return message ?? "Keychain error \(status)"
}
}
}
Require Face ID or Touch ID to read an item
For very sensitive values, attach an access control object so the system asks for biometrics before releasing the data. Add NSFaceIDUsageDescription to your Info.plist, or Face ID will not work.
import LocalAuthentication
extension KeychainStore {
func saveProtected(_ data: Data, account: String) throws {
var error: Unmanaged<CFError>?
guard let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet,
&error
) else {
throw error!.takeRetainedValue() as Error
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecAttrAccessControl as String: access,
kSecValueData as String: data
]
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandled(status) }
}
func readProtected(account: String, reason: String) throws -> Data {
let context = LAContext()
context.localizedReason = reason
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: kCFBooleanTrue as Any,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationContext as String: context
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess else { throw KeychainError.unhandled(status) }
guard let data = result as? Data else { throw KeychainError.unexpectedData }
return data
}
}
.biometryCurrentSet invalidates the item if the user adds or removes a fingerprint or face. Use .biometryAny if you do not want that, or .userPresence to allow the passcode as a fallback. Reading a protected item shows a system prompt and can block, so call it off the main thread.
Common Keychain mistakes
- Calling
SecItemAddfor an item that exists. You geterrSecDuplicateItem. Use thesavemethod above, which falls back to update. - Leaving out
kSecAttrService. Items from different features can collide. Always set it. - Setting both
kSecAttrAccessibleandkSecAttrAccessControl. Choose one. - Expecting the Keychain to be wiped on uninstall. Items can survive a reinstall. Check for a first-launch flag and clear old items if your app needs a clean start.
- Storing large or non-secret data. Keep the Keychain for secrets, and use another store for everything else.
- Logging secrets. Never print tokens or passwords, even in debug builds.
- Reading protected items on the main thread. Authentication prompts can freeze the UI.
FAQ
Q. Is the iOS Keychain secure?
A. Yes, for small secrets. The system encrypts items and ties access to your app and to the accessibility level you choose. No storage is perfect: a jailbroken device or a compromised account is outside what the Keychain can defend.
Q. What is the difference between the Keychain and UserDefaults?
A. UserDefaults stores values unencrypted and is meant for preferences. The Keychain encrypts values and controls when they can be read, so it is the place for passwords and tokens.
Q. Can I store a whole user object in the Keychain?
A. You can encode a small object to Data, but it is better to store only the secret (a token or key) in the Keychain and keep profile fields in normal app storage.
Q. Does the Keychain sync between devices?
A. Only if you set kSecAttrSynchronizable to true and the user has iCloud Keychain enabled. The ThisDeviceOnly accessibility values keep items on one device.
Q. Should I use a library instead of writing a wrapper?
A. A thin wrapper like the one above is enough for most apps. Libraries such as KeychainAccess or Valet add convenience, but you give up some control and gain a dependency.
Next steps
You now have a working Keychain layer for tokens and credentials. In Part II, we go further into practical patterns for managing user details. (Editor: confirm Part II’s current content and retitle it as the advanced follow-up, for example “iOS Keychain Access Groups, iCloud Sync and Testing”, so it no longer repeats the basics here.)
