Skill: Keychain
Overview
The Keychain is iOS's secure, encrypted store for small secrets: tokens, passwords, keys. It's the only acceptable place for credentials — never UserDefaults, plists, or files. Access is controlled by accessibility classes (when the item is readable) and optional access control (biometrics/passcode). Wrap the C API in a small, tested Swift abstraction and choose the least-permissive accessibility that still works for your use case.
Use Cases
- Storing OAuth2 access/refresh tokens and session credentials.
- Storing symmetric keys used for local encryption.
- Persisting a small secret that must survive app restarts but not leak via backups.
Best Practices
- Use
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlyfor tokens needing background access;WhenUnlockedThisDeviceOnlyfor stricter cases....ThisDeviceOnlyprevents iCloud/backups from carrying the secret. - Add
SecAccessControlwith biometrics for high-value secrets. - Update-or-add semantics (delete/add or
SecItemUpdate) to avoiderrSecDuplicateItem. - Clear all credentials on logout.
- Wrap in a protocol-backed, tested service; never scatter raw
SecItem*calls.
Anti-Patterns
- ❌ Storing tokens/keys in
UserDefaults, plists, or files. - ❌ Using the default (most permissive) accessibility unnecessarily.
- ❌ Syncing secrets to iCloud when they should be device-only.
- ❌ Ignoring
OSStatusresults. - ❌ Leaving credentials in the Keychain after logout.
Checklist
- [ ] Secrets only in Keychain, never
UserDefaults/files. - [ ] Least-permissive accessibility chosen;
...ThisDeviceOnlyfor non-synced secrets. - [ ] Biometric access control on high-value items where appropriate.
- [ ] Add/update handled (no duplicate errors);
OSStatuschecked. - [ ] Credentials cleared on logout.
Swift Examples
swift
protocol SecureStore {
func set(_ data: Data, for key: String) throws
func get(_ key: String) throws -> Data?
func remove(_ key: String) throws
}
struct KeychainStore: SecureStore {
let service: String
func set(_ data: Data, for key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
let attributes: [String: Any] = [
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
if status == errSecItemNotFound {
var add = query; add.merge(attributes) { _, new in new }
let addStatus = SecItemAdd(add as CFDictionary, nil)
guard addStatus == errSecSuccess else { throw KeychainError.status(addStatus) }
} else {
guard status == errSecSuccess else { throw KeychainError.status(status) }
}
}
func get(_ key: String) throws -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess else { throw KeychainError.status(status) }
return result as? Data
}
func remove(_ key: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw KeychainError.status(status)
}
}
}
enum KeychainError: Error { case status(OSStatus) }Common Interview Questions
- Why store tokens in the Keychain instead of
UserDefaults? - What do the accessibility classes control?
- What does
...ThisDeviceOnlychange? - How do you require Face ID/Touch ID to read an item?
- How do you avoid
errSecDuplicateItem?
AI Implementation Notes
- Always route credentials through a protocol-backed Keychain store; never
UserDefaults. - Default accessibility to
...AfterFirstUnlockThisDeviceOnly; tighten when possible. - Clear secrets on logout; generate tests using a fake
SecureStore. - Related:
oauth2.md,jwt.md,biometric_auth.md.