Skip to content

A modern, thread-safe Swift package for storing and retrieving Codable values in the iOS/macOS Keychain with support for Swift Concurrency.

License

Notifications You must be signed in to change notification settings

apparata/KeychainToolbox

Repository files navigation

KeychainToolbox

A modern, thread-safe Swift package for storing and retrieving Codable values in the iOS/macOS Keychain with support for Swift Concurrency.

Features

  • 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

Architecture

KeychainToolbox is built with a layered architecture:

  • KeychainOperations: Non-thread-safe low-level interface for Data operations with the Keychain Services API
  • CodableKeychainOperations: Non-thread-safe mid-level interface adding JSON encoding/decoding for Codable types
  • Keychain: High-level synchronous API with thread-safety via Mutex
  • AsyncKeychain: High-level actor-based API for Swift Concurrency thread-safety
  • @KeychainValue: Property wrapper for automatic persistence of optional values

Usage

KeychainToolbox provides multiple APIs for different use cases:

Property Wrapper (@KeychainValue)

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

Synchronous API (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")

Actor-based API (AsyncKeychain)

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")

Lower-Level APIs

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")

Configuration Options

Keychain Accessibility

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)

iCloud Keychain Sync

Enable syncing across the user's devices:

// Store with iCloud sync enabled
try keychain.addValue(token, for: "key", in: "namespace", cloud: true)

Namespacing

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)

Error Handling

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)")
}

Best Practices

  1. Choose the right API level:

    • Use @KeychainValue for simple property persistence
    • Use Keychain or AsyncKeychain for explicit control over operations
    • Use lower-level APIs (CodableKeychainOperations, KeychainOperations) for custom implementations
  2. Use namespaces to organize keys and avoid conflicts between different parts of your app

  3. Choose appropriate accessibility levels based on when you need access to the data

  4. Consider iCloud sync carefully - only enable for data that should sync across devices

  5. Handle errors appropriately - Keychain operations can fail for various reasons

  6. Use the Actor API when working with Swift Concurrency patterns

  7. Prefer addOrUpdateValue when you don't need to distinguish between creating and updating items

License

See the LICENSE file for licensing information.

About

A modern, thread-safe Swift package for storing and retrieving Codable values in the iOS/macOS Keychain with support for Swift Concurrency.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages