Storing Secrets in the Keychain
Save and retrieve credentials securely.
Storing Secrets in the Keychain is a free Swift Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Swift Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why the Keychain
Passwords, tokens, and keys must never sit in UserDefaults or plain files — those are easy to read. The Keychain is an encrypted, OS-managed database for small secrets, protected by hardware and the user's passcode. It is the only correct place for credentials.
import Security
// Keychain stores secrets encrypted at rest,
// survives app updates, and gates access by policy.Items Are Dictionaries
The Keychain Services API is C-based: you describe an item with a [String: Any] query dictionary using kSec... constant keys. The same dictionary shape is reused for add, search, update, and delete.
import Security
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "user@example.com",
kSecAttrService as String: "com.example.app"
]
_ = queryItem Classes
The kSecClass key picks the item type. kSecClassGenericPassword covers app tokens and secrets; kSecClassInternetPassword stores server credentials with host/protocol attributes. Most app secrets use generic password.
import Security
// kSecClassGenericPassword -> tokens, API keys
// kSecClassInternetPassword -> server logins
// kSecClassKey / Certificate -> crypto material
let cls = kSecClassGenericPassword
_ = clsAdding an Item
SecItemAdd inserts a new item. You include the data under kSecValueData as Data, plus attributes that identify it. It returns an OSStatus — errSecSuccess means it worked.
import Security
func save(_ token: String, account: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecValueData as String: Data(token.utf8)
]
return SecItemAdd(query as CFDictionary, nil)
== errSecSuccess
}Handling Duplicates
Adding an item whose identifying attributes already exist returns errSecDuplicateItem. A robust save tries SecItemAdd, and on duplicate falls back to SecItemUpdate — an upsert pattern.
import Security
func upsert(_ data: Data, account: String) -> Bool {
let base: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account]
var add = base
add[kSecValueData as String] = data
let status = SecItemAdd(add as CFDictionary, nil)
if status == errSecDuplicateItem {
return SecItemUpdate(base as CFDictionary,
[kSecValueData as String: data] as CFDictionary)
== errSecSuccess
}
return status == errSecSuccess
}Reading an Item Back
SecItemCopyMatching searches. To get the secret bytes you must set kSecReturnData to true and kSecMatchLimit to kSecMatchLimitOne. The result comes back through an out-parameter as CFTypeRef.
import Security
func load(account: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne]
var result: CFTypeRef?
let status = SecItemCopyMatching(
query as CFDictionary, &result)
guard status == errSecSuccess else { return nil }
return result as? Data
}Updating an Item
SecItemUpdate takes two dictionaries: a query that finds the item and an attributes-to-update dictionary. Only the attributes you list change; everything else is preserved.
import Security
func update(_ newData: Data, account: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account]
let attrs: [String: Any] = [
kSecValueData as String: newData]
return SecItemUpdate(query as CFDictionary,
attrs as CFDictionary) == errSecSuccess
}Deleting an Item
SecItemDelete removes matching items. Deleting something that does not exist returns errSecItemNotFound, which you can treat as success when clearing credentials on logout.
import Security
func delete(account: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: account]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess
|| status == errSecItemNotFound
}Decoding OSStatus
Errors are integer OSStatus codes. SecCopyErrorMessageString turns one into a human-readable description, which is invaluable when debugging why a save or read failed.
import Security
func describe(_ status: OSStatus) -> String {
return SecCopyErrorMessageString(status, nil)
as String? ?? "OSStatus \(status)"
}Identifying Items Uniquely
Items are matched by their combination of attributes — typically kSecAttrService plus kSecAttrAccount for generic passwords. Choose a stable, app-specific service string so different secrets never collide.
import Security
// Uniqueness for generic passwords usually comes from:
// service (your bundle id) + account (the username)
let service = "com.example.app.auth"
let account = "current-user"
_ = (service, account)A Small Wrapper
Because the raw API is verbose, teams wrap it in a tiny type exposing save, read, and delete. This keeps the kSec ceremony in one place and the call sites clean.
import Security
struct TokenStore {
let service = "com.example.app.auth"
func read(_ account: String) -> Data? {
let q: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne]
var out: CFTypeRef?
return SecItemCopyMatching(q as CFDictionary, &out)
== errSecSuccess ? out as? Data : nil
}
}Quick Check
Recall the correct way to retrieve a stored secret's bytes.
Recap
You learned Keychain CRUD:
- Store credentials in the encrypted Keychain, never in UserDefaults or files.
- Describe items with
kSecquery dictionaries; pick a class likekSecClassGenericPassword. SecItemAdd/SecItemCopyMatching/SecItemUpdate/SecItemDeletecover the lifecycle; handleerrSecDuplicateItemwith an upsert.- Identify items by service + account, and decode
OSStatusfor debugging.
Frequently asked questions
Is the “Storing Secrets in the Keychain” lesson free?
Yes — the full text of “Storing Secrets in the Keychain” is free to read here on the web, and the Swift Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Swift Academy course, upgrade to CoddyKit PRO.
What will I learn in “Storing Secrets in the Keychain”?
Save and retrieve credentials securely. You practise Swift Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Swift Academy?
No prior experience is required. Swift Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Storing Secrets in the Keychain” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Swift Academy lesson?
Yes. Every Swift Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Storing Secrets in the Keychain
- Keychain Access Control
- Biometric Authentication
- Data Protection and Encryption