Which OSStatus values matter when you use SecItem functions, and how do you write a correct save (upsert) of a secret?
Predict first
What does each Keychain call return?
import Foundation
import Security
let service = "test.\(UUID().uuidString)"
func query(_ account: String) -> [String: Any] {
[kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account]
}
func add(_ account: String, _ value: String) -> OSStatus {
var q = query(account)
q[kSecValueData as String] = Data(value.utf8)
return SecItemAdd(q as CFDictionary, nil)
}
print("add:", add("alice", "one"))
print("add again, errSecDuplicateItem:", add("alice", "two") == errSecDuplicateItem)
print("update:", SecItemUpdate(query("alice") as CFDictionary, [kSecValueData as String: Data("three".utf8)] as CFDictionary))
var q = query("alice")
q[kSecReturnData as String] = true
q[kSecMatchLimit as String] = kSecMatchLimitOne
var out: CFTypeRef?
print("read:", SecItemCopyMatching(q as CFDictionary, &out), String(decoding: (out as? Data) ?? Data(), as: UTF8.self))
print("read missing, errSecItemNotFound:", SecItemCopyMatching(query("bob") as CFDictionary, &out) == errSecItemNotFound)
print("update missing, errSecItemNotFound:", SecItemUpdate(query("bob") as CFDictionary, [kSecValueData as String: Data("x".utf8)] as CFDictionary) == errSecItemNotFound)
print("delete:", SecItemDelete(query("alice") as CFDictionary), "| delete again not found:", SecItemDelete(query("alice") as CFDictionary) == errSecItemNotFound)
- add: 0 / add again, errSecDuplicateItem: true / update: 0 / read: 0 three / read missing: true / update missing: true / delete: 0 | delete again not found: true
- add: 0 / add again: 0
- read: -25300
Mental model. The keychain is a safe with a clerk who answers in numbers: “already exists”, “not found”, “locked right now”. Your code must know what each number means and what to do next.
The Keychain API returns an OSStatus for everything, and the useful values are few. errSecSuccess (0); errSecDuplicateItem (−25299) when SecItemAdd finds an item that matches the primary key attributes (for a generic password: class, service, account, access group); errSecItemNotFound (−25300) from a read, update or delete with no match; errSecInteractionNotAllowed (−25308) when the item is protected and the device is locked (an item with kSecAttrAccessibleWhenUnlocked read from a background task); and errSecMissingEntitlement / −34018 when the keychain access group or entitlement is not set up (common in tests and extensions). The check shows the core sequence: add succeeds (0), a second add returns “duplicate”, an update changes the value, a read returns it, and a read or update on a missing item returns “not found”.
That gives the right upsert: try SecItemUpdate first; if it returns errSecItemNotFound then SecItemAdd; or add first and on errSecDuplicateItem update. Never delete then add (a crash between them loses the secret). Treat the query dictionary as the identity: build it in one function, include a service and account, set kSecAttrAccessible deliberately (AfterFirstUnlockThisDeviceOnly for background-readable tokens that must not move to a new device) and kSecUseDataProtectionKeychain on macOS. Wrap the C API in a small type that throws a typed error carrying the status, never log secret values, and write tests that use a unique service name per run and delete in teardown (the check does, and cleans up after itself). Remember Keychain items survive an app deletion on iOS in some cases, so first launch after reinstall should decide whether to trust a stored token (Q328).
Add fails with duplicate on a second add; update and read work on an existing item and report not-found otherwise.
Common traps
- Using delete then add to “replace” a secret. A failure in between loses it; update or add-then-update.
- Reading a protected item from a background task on a locked device. You get
errSecInteractionNotAllowed; choose the accessibility class for how the item is used.
They might follow up with
- What makes two items “the same”? The class’s primary key attributes; for generic passwords, service plus account (plus access group).
- How do I store a Codable value? Encode to
Dataand store it as the value data; keep it small.
Related questions
- 13.8.1 What does UserDefaults return for a key that is not set, how do registered defaults work, and what should and…
- 13.11.2 How do you detect a damaged database or file, and what do you do about it?
- 13.1.1 UserDefaults, Keychain, files, SQLite, Core Data or SwiftData: how do you choose, and what survives deletion…
- 13.1.2 How does the Keychain work, how do you add, read and update an item, and what do accessibility classes and…
How do you detect a damaged database or file, and what do you do… →