npx claudepluginhub charleswiltgen/axiom --plugin axiomThis skill uses the workspace's default tool permissions.
Comprehensive API reference for iOS/macOS Keychain Services: SecItem CRUD functions, item class attributes, uniqueness constraints, accessibility levels, access control flags, biometric integration, and error codes.
Provides Ktor server patterns for routing DSL, plugins (auth, CORS, serialization), Koin DI, WebSockets, services, and testApplication testing.
Conducts multi-source web research with firecrawl and exa MCPs: searches, scrapes pages, synthesizes cited reports. For deep dives, competitive analysis, tech evaluations, or due diligence.
Provides demand forecasting, safety stock optimization, replenishment planning, and promotional lift estimation for multi-location retailers managing 300-800 SKUs.
Comprehensive API reference for iOS/macOS Keychain Services: SecItem CRUD functions, item class attributes, uniqueness constraints, accessibility levels, access control flags, biometric integration, and error codes.
// Add a generic password
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "user@example.com",
kSecValueData as String: "secret".data(using: .utf8)!
]
let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
// Read a generic password
let readQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "user@example.com",
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let readStatus = SecItemCopyMatching(readQuery as CFDictionary, &result)
let data = result as? Data
// Update a generic password
let updateQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "user@example.com"
]
let updateAttributes: [String: Any] = [
kSecValueData as String: "newSecret".data(using: .utf8)!
]
let updateStatus = SecItemUpdate(updateQuery as CFDictionary, updateAttributes as CFDictionary)
// Delete a generic password
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "user@example.com"
]
let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
func SecItemAdd(_ attributes: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
attributes dictionary accepts: Item class + item attributes + value properties + return type properties.
Does NOT accept search properties (kSecMatch*). Providing kSecMatchLimit in an add query is an error.
result: Pass nil if you don't need the added item back. Pass a pointer to receive the item in the format specified by kSecReturn* keys. Pass nil in most cases — requesting the result back forces an extra read.
func SecItemCopyMatching(_ query: CFDictionary, _ result: UnsafeMutablePointer<CFTypeRef?>?) -> OSStatus
query dictionary accepts: Item class + item attributes + search properties + return type properties.
Does NOT accept value properties (kSecValueData) as search criteria.
result: The type depends on which kSecReturn* keys are set:
kSecReturnData alone → CFDatakSecReturnAttributes alone → CFDictionarykSecReturnRef alone → SecKey / SecCertificate / SecIdentitykSecReturnPersistentRef alone → CFData (persistent reference)kSecReturn* keys → CFDictionary containing requested typeskSecMatchLimit = kSecMatchLimitAll → CFArray of the abovefunc SecItemUpdate(_ query: CFDictionary, _ attributesToUpdate: CFDictionary) -> OSStatus
query dictionary accepts: Item class + item attributes + search properties. Used to find items to update.
attributesToUpdate dictionary accepts: Item attributes + value properties. These are applied to matched items. Does NOT accept item class or search properties.
Updating kSecValueData replaces the stored secret. Updating attributes (e.g., kSecAttrLabel) changes metadata without touching the secret.
func SecItemDelete(_ query: CFDictionary) -> OSStatus
query dictionary accepts: Item class + item attributes + search properties.
On macOS, deletes ALL matching items by default (implicit kSecMatchLimitAll). On iOS, also deletes all matches. There is no confirmation — deletion is immediate.
General-purpose secret storage. The most commonly used class.
| Attribute | Key | Type |
|---|---|---|
| Service | kSecAttrService | CFString |
| Account | kSecAttrAccount | CFString |
| Access Group | kSecAttrAccessGroup | CFString |
| Accessible | kSecAttrAccessible | CFString (constant) |
| Synchronizable | kSecAttrSynchronizable | CFBoolean |
| Label | kSecAttrLabel | CFString |
| Description | kSecAttrDescription | CFString |
| Comment | kSecAttrComment | CFString |
| Generic | kSecAttrGeneric | CFData |
| Creator | kSecAttrCreator | CFNumber (FourCharCode) |
| Type | kSecAttrType | CFNumber (FourCharCode) |
| Creation Date | kSecAttrCreationDate | CFDate (read-only) |
| Modification Date | kSecAttrModificationDate | CFDate (read-only) |
URL-associated credentials. Rarely needed — most apps use generic passwords.
| Attribute | Key | Type |
|---|---|---|
| Server | kSecAttrServer | CFString |
| Protocol | kSecAttrProtocol | CFString (constant) |
| Port | kSecAttrPort | CFNumber |
| Path | kSecAttrPath | CFString |
| Account | kSecAttrAccount | CFString |
| Authentication Type | kSecAttrAuthenticationType | CFString (constant) |
| Security Domain | kSecAttrSecurityDomain | CFString |
| Accessible | kSecAttrAccessible | CFString (constant) |
| Access Group | kSecAttrAccessGroup | CFString |
| Synchronizable | kSecAttrSynchronizable | CFBoolean |
| Label | kSecAttrLabel | CFString |
| Comment | kSecAttrComment | CFString |
| Creator | kSecAttrCreator | CFNumber (FourCharCode) |
| Type | kSecAttrType | CFNumber (FourCharCode) |
X.509 certificates. Typically managed by the system, not app code.
| Attribute | Key | Type |
|---|---|---|
| Subject | kSecAttrSubject | CFData (read-only) |
| Issuer | kSecAttrIssuer | CFData (read-only) |
| Serial Number | kSecAttrSerialNumber | CFData (read-only) |
| Subject Key ID | kSecAttrSubjectKeyID | CFData (read-only) |
| Public Key Hash | kSecAttrPublicKeyHash | CFData (read-only) |
| Certificate Type | kSecAttrCertificateType | CFNumber |
| Certificate Encoding | kSecAttrCertificateEncoding | CFNumber |
| Label | kSecAttrLabel | CFString |
| Access Group | kSecAttrAccessGroup | CFString |
| Synchronizable | kSecAttrSynchronizable | CFBoolean |
Cryptographic keys (RSA, EC, AES). Used for encryption, signing, key agreement.
| Attribute | Key | Type |
|---|---|---|
| Key Class | kSecAttrKeyClass | CFString (constant) |
| Application Label | kSecAttrApplicationLabel | CFData |
| Application Tag | kSecAttrApplicationTag | CFData |
| Key Type | kSecAttrKeyType | CFString (constant) |
| Key Size in Bits | kSecAttrKeySizeInBits | CFNumber |
| Effective Key Size | kSecAttrEffectiveKeySize | CFNumber |
| Permanent | kSecAttrIsPermanent | CFBoolean |
| Sensitive | kSecAttrIsSensitive | CFBoolean |
| Extractable | kSecAttrIsExtractable | CFBoolean |
| Label | kSecAttrLabel | CFString |
| Access Group | kSecAttrAccessGroup | CFString |
| Synchronizable | kSecAttrSynchronizable | CFBoolean |
| Token ID | kSecAttrTokenID | CFString |
A digital identity is a certificate paired with its private key. Not a distinct storage class — the system synthesizes it from a matching certificate and key. You cannot add a kSecClassIdentity item directly; add the certificate and key separately. Queries return an identity when both halves share the same kSecAttrPublicKeyHash.
See Quinn "The Eskimo!"'s technote: "SecItem: Pitfalls and Best Practices" (forums/thread/724013) — digital identities are a virtual join, not a stored item.
Each keychain item is uniquely identified by a subset of its attributes. Adding a second item with the same primary key returns errSecDuplicateItem (-25299). Use SecItemUpdate to modify existing items.
| Class | Primary Key Attributes |
|---|---|
| Generic Password | kSecAttrService + kSecAttrAccount + kSecAttrAccessGroup + kSecAttrSynchronizable |
| Internet Password | kSecAttrServer + kSecAttrPort + kSecAttrProtocol + kSecAttrAuthenticationType + kSecAttrPath + kSecAttrAccount + kSecAttrAccessGroup + kSecAttrSynchronizable |
| Certificate | kSecAttrCertificateType + kSecAttrIssuer + kSecAttrSerialNumber + kSecAttrAccessGroup + kSecAttrSynchronizable |
| Key | kSecAttrApplicationLabel + kSecAttrApplicationTag + kSecAttrKeyType + kSecAttrKeySizeInBits + kSecAttrEffectiveKeySize + kSecAttrKeyClass + kSecAttrAccessGroup + kSecAttrSynchronizable |
| Identity | N/A (virtual join of certificate + key) |
Consequence: If you store tokens for multiple users under the same kSecAttrService without unique kSecAttrAccount values, SecItemAdd returns errSecDuplicateItem for the second user.
| Constant | Type | Used By |
|---|---|---|
kSecAttrService | CFString | GenericPassword |
kSecAttrAccount | CFString | GenericPassword, InternetPassword |
kSecAttrServer | CFString | InternetPassword |
kSecAttrLabel | CFString | All classes |
kSecAttrDescription | CFString | GenericPassword, InternetPassword |
kSecAttrComment | CFString | GenericPassword, InternetPassword |
kSecAttrGeneric | CFData | GenericPassword |
| Constant | Type | Used By |
|---|---|---|
kSecAttrAccessible | CFString (constant) | All classes |
kSecAttrAccessControl | SecAccessControl | All classes |
kSecAttrAccessGroup | CFString | All classes |
kSecAttrSynchronizable | CFBoolean | All classes |
kSecAttrAccessible and kSecAttrAccessControl are mutually exclusive. Setting both is an error — kSecAttrAccessControl includes an accessibility level in its creation.
| Constant | Type | Purpose |
|---|---|---|
kSecAttrTokenID | CFString | Bind key to hardware token |
kSecAttrTokenIDSecureEnclave | CFString (value) | Secure Enclave — EC keys only (256-bit) |
| Constant | Type | Values |
|---|---|---|
kSecAttrKeyType | CFString | kSecAttrKeyTypeRSA, kSecAttrKeyTypeECSECPrimeRandom |
kSecAttrKeySizeInBits | CFNumber | 256 (EC), 2048/4096 (RSA) |
kSecAttrKeyClass | CFString | kSecAttrKeyClassPublic, kSecAttrKeyClassPrivate, kSecAttrKeyClassSymmetric |
kSecAttrApplicationTag | CFData | App-defined tag for key lookup |
kSecAttrApplicationLabel | CFData | SHA-1 hash of public key (auto-generated) |
Used in SecItemCopyMatching, SecItemUpdate (query parameter), and SecItemDelete queries.
| Constant | Type | Purpose |
|---|---|---|
kSecMatchLimit | CFString or CFNumber | Max results — kSecMatchLimitOne, kSecMatchLimitAll, or CFNumber for explicit integer limits (e.g., limit to 5 results) |
kSecMatchCaseInsensitive | CFBoolean | Case-insensitive string attribute matching |
The default depends on context and is a common source of bugs:
| Function | Default | Behavior |
|---|---|---|
SecItemCopyMatching | kSecMatchLimitOne | Returns first match |
SecItemDelete | All matches | Deletes every matching item |
Always set kSecMatchLimit explicitly in SecItemCopyMatching to make intent clear. For SecItemDelete, omitting kSecMatchLimit deletes all matches — this is by design, not a bug.
Control what SecItemCopyMatching and SecItemAdd return. Set in the query dictionary.
| Constant | Returns | Result Type |
|---|---|---|
kSecReturnData | The secret (password bytes, key data) | CFData |
kSecReturnAttributes | Item metadata dictionary | CFDictionary |
kSecReturnRef | Keychain object reference | SecKey, SecCertificate, or SecIdentity |
kSecReturnPersistentRef | Persistent reference (survives app relaunch) | CFData |
| Class | kSecReturnData | kSecReturnRef |
|---|---|---|
| Generic Password | Password bytes | N/A (no ref type) |
| Internet Password | Password bytes | N/A (no ref type) |
| Certificate | DER-encoded certificate data | SecCertificate |
| Key | Key data (if extractable) | SecKey |
| Identity | N/A | SecIdentity |
When multiple kSecReturn* keys are true, the result is a CFDictionary with keys:
kSecValueData → the datakSecValueRef → the refkSecValuePersistentRef → the persistent refkSecReturnAttributes is trueWhen kSecMatchLimitAll is set, the result is a CFArray of the above.
Used to provide or extract values in add, query, and update dictionaries.
| Constant | Type | Purpose |
|---|---|---|
kSecValueData | CFData | The secret (password, key material) |
kSecValueRef | SecKey / SecCertificate / SecIdentity | Keychain object reference |
kSecValuePersistentRef | CFData | Persistent reference to an item |
| Property | SecItemAdd | SecItemCopyMatching | SecItemUpdate |
|---|---|---|---|
kSecValueData | Sets the secret | Not valid as search criteria | Replaces the secret |
kSecValueRef | Adds the referenced object | Finds by reference | Not valid |
kSecValuePersistentRef | Not valid | Finds by persistent ref | Not valid |
Controls when keychain items are readable. Set via kSecAttrAccessible.
| Constant | Available When | Survives Backup | Syncs via iCloud |
|---|---|---|---|
kSecAttrAccessibleWhenUnlocked | Device unlocked | Yes | Yes (default) |
kSecAttrAccessibleAfterFirstUnlock | After first unlock until reboot | Yes | Yes |
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly | Device unlocked + passcode set | No | No |
kSecAttrAccessibleWhenUnlockedThisDeviceOnly | Device unlocked | No | No |
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly | After first unlock until reboot | No | No |
Default: kSecAttrAccessibleWhenUnlocked for new items.
ThisDeviceOnly variants: Item is not included in encrypted backups and does not sync via iCloud Keychain. Use for device-bound secrets (biometric-gated tokens, Secure Enclave keys).
WhenPasscodeSetThisDeviceOnly: Item is deleted if the user removes their passcode. Use for secrets that must not survive passcode removal.
AfterFirstUnlock: Available in the background after the user unlocks once post-reboot. Required for background fetch, push notification handlers, and background URLSession completions.
Deprecated (do not use): kSecAttrAccessibleAlways, kSecAttrAccessibleAlwaysThisDeviceOnly.
Fine-grained access control for keychain items. Created with SecAccessControlCreateWithFlags and set via kSecAttrAccessControl.
| Flag | Purpose |
|---|---|
.userPresence | Any biometric OR device passcode |
.biometryAny | Any enrolled biometric (survives new enrollment) |
.biometryCurrentSet | Current biometric set only (invalidated if biometrics change) |
.devicePasscode | Device passcode required |
.privateKeyUsage | Required for Secure Enclave key signing operations |
.applicationPassword | App-provided password (in addition to other factors) |
.watch | Paired Apple Watch can satisfy authentication |
.or | Combine flags with logical OR (any one satisfies) |
.and | Combine flags with logical AND (all must satisfy) |
var error: Unmanaged<CFError>?
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
[.biometryCurrentSet, .or, .devicePasscode],
&error
) else {
let nsError = error!.takeRetainedValue() as Error
fatalError("Failed to create access control: \(nsError)")
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "auth-token",
kSecAttrAccessControl as String: accessControl,
kSecValueData as String: tokenData
]
let status = SecItemAdd(query as CFDictionary, nil)
| Combination | Meaning |
|---|---|
[.biometryAny] | Any enrolled fingerprint/face |
[.biometryCurrentSet] | Current fingerprint/face set (re-enroll invalidates) |
[.biometryCurrentSet, .or, .devicePasscode] | Biometric OR passcode fallback |
[.biometryCurrentSet, .and, .applicationPassword] | Biometric AND app password |
[.privateKeyUsage] | Secure Enclave key operations (sign, decrypt) |
[.biometryAny, .or, .watch] | Biometric OR paired Watch |
.biometryAny vs .biometryCurrentSet: Use .biometryCurrentSet for high-security items (banking tokens). If the user enrolls a new fingerprint, the item becomes inaccessible — your app must re-authenticate and re-store. Use .biometryAny for convenience items where new enrollment should not invalidate access.
Pre-evaluate biometrics with LAContext, then pass the context to the keychain query to avoid a second biometric prompt.
import LocalAuthentication
let context = LAContext()
context.localizedReason = "Access your credentials"
var authError: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError) else {
// Biometrics unavailable — handle error or fall back to passcode
return
}
context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to access credentials"
) { success, error in
guard success else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "auth-token",
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecUseAuthenticationContext as String: context
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
}
| Key | Type | Purpose |
|---|---|---|
kSecUseAuthenticationContext | LAContext | Reuse authenticated context (avoids double prompt) |
kSecUseAuthenticationUI | CFString | Control UI behavior: kSecUseAuthenticationUIAllow (default), kSecUseAuthenticationUIFail, kSecUseAuthenticationUISkip |
kSecUseAuthenticationUIFail: Returns errSecInteractionNotAllowed instead of showing the biometric prompt. Use to check if an item exists without triggering UI.
| Policy | Requires |
|---|---|
.deviceOwnerAuthenticationWithBiometrics | Face ID or Touch ID only |
.deviceOwnerAuthentication | Biometrics or passcode fallback |
let context = LAContext()
var error: NSError?
context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
switch context.biometryType {
case .faceID: // Face ID available
case .touchID: // Touch ID available
case .opticID: // Optic ID available (visionOS)
case .none: // No biometric hardware
@unknown default: break
}
| Error | Code | Cause |
|---|---|---|
.authenticationFailed | -1 | User failed authentication |
.userCancel | -2 | User tapped Cancel |
.userFallback | -3 | User tapped "Enter Password" |
.systemCancel | -4 | System cancelled (app backgrounded) |
.passcodeNotSet | -5 | No passcode configured |
.biometryNotAvailable | -6 | Hardware unavailable or restricted |
.biometryNotEnrolled | -7 | No biometrics enrolled |
.biometryLockout | -8 | Too many failed attempts |
Common keychain OSStatus values and their root causes.
| Error | Code | Description | Common Cause |
|---|---|---|---|
errSecSuccess | 0 | Operation succeeded | — |
errSecDuplicateItem | -25299 | Item already exists | Adding with same primary key — use SecItemUpdate instead |
errSecItemNotFound | -25300 | No matching item | Wrong query attributes or item never stored |
errSecInteractionNotAllowed | -25308 | UI prompt blocked | Item requires auth but device locked, or kSecUseAuthenticationUIFail set |
errSecAuthFailed | -25293 | Authentication failed | Wrong password, failed biometric, or ACL denied |
errSecMissingEntitlement | -34018 | Missing keychain entitlement | App lacks keychain-access-groups entitlement — common in unit test targets |
errSecNoSuchAttr | -25303 | Attribute not found | Querying an attribute not valid for the item class |
errSecParam | -50 | Invalid parameter | Malformed query dictionary — check for type mismatches (e.g., String where Data expected) |
errSecAllocate | -108 | Memory allocation failed | System resource exhaustion |
errSecDecode | -26275 | Unable to decode data | Corrupted item or encoding mismatch |
errSecNotAvailable | -25291 | Keychain not available | No keychain database (rare — Simulator reset or corrupted install) |
let status = SecItemAdd(query as CFDictionary, nil)
if status != errSecSuccess {
let message = SecCopyErrorMessageString(status, nil) as? String ?? "Unknown error"
print("Keychain error \(status): \(message)")
}
Unit test runners (XCTest) often lack the keychain-access-groups entitlement. Workarounds:
Items are isolated per app by default. To share between apps or extensions:
kSecAttrAccessGroup when adding itemslet query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.shared",
kSecAttrAccount as String: "shared-token",
kSecAttrAccessGroup as String: "TEAMID.com.example.shared",
kSecValueData as String: tokenData
]
The access group format is $(TeamIdentifierPrefix)$(GroupIdentifier). Items without an explicit access group default to the app's first access group in its entitlements.
Set kSecAttrSynchronizable to true to sync via iCloud Keychain:
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: "com.example.app",
kSecAttrAccount as String: "sync-token",
kSecAttrSynchronizable as String: true,
kSecValueData as String: tokenData
]
Synchronizable items cannot use ThisDeviceOnly accessibility levels or SecAccessControl. They must use kSecAttrAccessibleWhenUnlocked or kSecAttrAccessibleAfterFirstUnlock.
When querying, kSecAttrSynchronizable defaults to kSecAttrSynchronizableAny (returns both local and synced items). Set explicitly to true or false to filter.
WWDC: 2013-709, 2014-711, 2020-10147
Docs: /security/keychain_services, /localauthentication, /security/secaccesscontrolcreateflags, /security/secitemadd(::)
Skills: axiom-code-signing-ref, axiom-app-attest