A modern, thread-safe Swift package for storing and retrieving Codable
values in the iOS/macOS Keychain with support for Swift Concurrency.
- Thread-Safe: All high-level operations are protected with locks/actor isolation
- Codable Support: Store and retrieve any
Codable
type directly - Property Wrapper: Simple
@KeychainValue
wrapper for automatic persistence - Namespace Support: Organize keys to avoid collisions between features
- iCloud Keychain: Optional syncing across user's devices
- Swift Concurrency: Actor-based API for async/await patterns
- Cross-Platform: Supports iOS 16+, macOS 14+, tvOS 16+, and visionOS 1+
- Layered Architecture: Multiple abstraction levels from raw data to high-level APIs
KeychainToolbox is built with a layered architecture:
KeychainOperations
: Non-thread-safe low-level interface forData
operations with the Keychain Services APICodableKeychainOperations
: Non-thread-safe mid-level interface adding JSON encoding/decoding forCodable
typesKeychain
: High-level synchronous API with thread-safety viaMutex
AsyncKeychain
: High-level actor-based API for Swift Concurrency thread-safety@KeychainValue
: Property wrapper for automatic persistence of optional values
KeychainToolbox provides multiple APIs for different use cases:
The simplest way to persist values - for iOS 18+, macOS 15+, tvOS 18+, visionOS 1+:
import KeychainToolbox
struct Settings {
@KeychainValue("authToken", namespace: "com.example.auth")
var authToken: String?
@KeychainValue("userPreferences")
var preferences: UserPreferences?
@KeychainValue("apiKey", namespace: "com.example.api", cloud: true)
var apiKey: String?
}
// Usage is automatic
var settings = Settings()
settings.authToken = "abc123" // Automatically stored in keychain
let token = settings.authToken // Automatically retrieved from keychain
settings.authToken = nil // Automatically deleted from keychain
The Keychain
class provides thread-safe keychain operations, protected with a mutex internally.
It should be noted that it is only thread-safe in that each instance is protected from interfering with itself. Two separate instances that are used at the same time will not be thread-safe. Therefore it may be a good idea to use the Keychain.shared
keychain instance.
Note: The Keychain
class requires iOS 18+, macOS 15+, tvOS 18+, or visionOS 1+ due to its use of the Synchronization
framework for a modern Mutex
.
import KeychainToolbox
let keychain = Keychain.shared()
struct AuthToken: Codable {
let accessToken: String
let refreshToken: String
let expiresAt: Date
}
// Store a value
let token = AuthToken(accessToken: "abc123", refreshToken: "xyz789", expiresAt: Date())
try keychain.addValue(token, for: "authToken", in: "com.example.app")
// Read a value
let storedToken: AuthToken? = try keychain.value(for: "authToken", in: "com.example.app")
// Update a value
let newToken = AuthToken(accessToken: "def456", refreshToken: "uvw012", expiresAt: Date())
try keychain.updateValue(newToken, for: "authToken", in: "com.example.app")
// Add or update (creates if missing, updates if exists)
try keychain.addOrUpdateValue(token, for: "authToken", in: "com.example.app")
// Delete a value
try keychain.deleteValue(for: "authToken", in: "com.example.app")
The thread-safety of the AsyncKeychain
class is provided by the @KeychainActor
. The downside of actor-based functions is that they are all suspension points. To avoid suspension points, use Keychain
class or the @KeychainValue
property wrapper instead.
import KeychainToolbox
let keychain = AsyncKeychain()
// Store a value
try await keychain.addValue(token, for: "authToken", in: "com.example.app")
// Read a value
let storedToken: AuthToken? = try await keychain.value(for: "authToken", in: "com.example.app")
// Update a value
try await keychain.updateValue(newToken, for: "authToken", in: "com.example.app")
// Add or update (creates if missing, updates if exists)
try await keychain.addOrUpdateValue(token, for: "authToken", in: "com.example.app")
// Delete a value
try await keychain.deleteValue(for: "authToken", in: "com.example.app")
For advanced use cases, you can use the lower-level components directly:
// Direct operations on Data
let operations = KeychainOperations()
let data = "secret".data(using: .utf8)!
try operations.addValue(data, for: "key", in: "namespace")
// Operations on Codable types
let codableOps = CodableKeychainOperations()
try codableOps.addValue(myStruct, for: "key", in: "namespace")
Control when your data is accessible:
// Data accessible only when device is unlocked (default)
try keychain.addValue(token, for: "key", in: "namespace", access: .whenUnlocked)
// Data accessible after first unlock since restart
try keychain.addValue(token, for: "key", in: "namespace", access: .afterFirstUnlock)
// Data accessible only on this device when unlocked
try keychain.addValue(token, for: "key", in: "namespace", access: .whenUnlockedThisDeviceOnly)
Enable syncing across the user's devices:
// Store with iCloud sync enabled
try keychain.addValue(token, for: "key", in: "namespace", cloud: true)
Use namespaces to organize keys and avoid collisions:
// Different namespaces, same key
try keychain.addValue(userToken, for: "token", in: "com.example.auth")
try keychain.addValue(apiToken, for: "token", in: "com.example.api")
// No namespace (pass nil)
try keychain.addValue(someValue, for: "globalKey", in: nil)
KeychainToolbox uses a comprehensive error enum:
do {
let value: MyType? = try keychain.value(for: "key", in: "namespace")
} catch KeychainError.valueLookupFailed(let status) {
print("Failed to lookup value with status: \(status)")
} catch KeychainError.valueLookupReturnedInvalidData {
print("Data in keychain couldn't be decoded")
} catch KeychainError.addValueFailed(let status) {
print("Failed to add value with status: \(status)")
} catch KeychainError.updateValueFailed(let status) {
print("Failed to update value with status: \(status)")
} catch KeychainError.deleteValueFailed(let status) {
print("Failed to delete value with status: \(status)")
}
-
Choose the right API level:
- Use
@KeychainValue
for simple property persistence - Use
Keychain
orAsyncKeychain
for explicit control over operations - Use lower-level APIs (
CodableKeychainOperations
,KeychainOperations
) for custom implementations
- Use
-
Use namespaces to organize keys and avoid conflicts between different parts of your app
-
Choose appropriate accessibility levels based on when you need access to the data
-
Consider iCloud sync carefully - only enable for data that should sync across devices
-
Handle errors appropriately - Keychain operations can fail for various reasons
-
Use the Actor API when working with Swift Concurrency patterns
-
Prefer
addOrUpdateValue
when you don't need to distinguish between creating and updating items
See the LICENSE file for licensing information.