Modern, type-safe Keychain wrapper with Codable, biometric auth, and async/await
.package(url: "https://github.com/philiprehberger/swift-keychain-kit.git", from: "0.2.0")Modern, type-safe Keychain wrapper with Codable, biometric auth, and async/await
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/philiprehberger/swift-keychain-kit.git", from: "0.1.0")
]
Then add "KeychainKit" to your target dependencies:
.target(name: "YourTarget", dependencies: [
.product(name: "KeychainKit", package: "swift-keychain-kit")
])
import KeychainKit
let keychain = Keychain(service: "com.myapp")
try keychain.set("secret-token", for: "api-key")
let token = try keychain.string(for: "api-key") // => "secret-token"
Store any Codable type as JSON in the Keychain:
struct Credentials: Codable, Sendable {
let username: String
let token: String
}
let creds = Credentials(username: "alice", token: "abc123")
try keychain.set(creds, for: "credentials")
let restored = try keychain.object(for: "credentials", as: Credentials.self)
// => Credentials(username: "alice", token: "abc123")
Protect items with Face ID or Touch ID:
try keychain.setWithBiometric(
creds,
for: "secure-creds",
policy: .biometricAny,
prompt: "Authenticate to access credentials"
)
let secured = try await keychain.objectWithBiometric(
for: "secure-creds",
as: Credentials.self
)
Control when items are accessible:
try keychain.set("value", for: "key", access: .afterFirstUnlock)
try keychain.set("value", for: "key", access: .whenPasscodeSet)
Rotate keys atomically:
try KeyRotation.rotate(in: keychain, from: "v1.token", to: "v2.token")
// Batch rotation with prefix
let count = try KeyRotation.rotateAll(
in: keychain,
matchingPrefix: "v1."
) { $0.replacingOccurrences(of: "v1.", with: "v2.") }
Store values that expire after a given time:
try keychain.set("temp-token", for: "session", expiresIn: 3600) // 1 hour
try keychain.isExpired("session") // false
// Clean up all expired items
let removed = try keychain.cleanExpired()
Retrieve strings and raw data with biometric protection:
let token = try await keychain.stringWithBiometric(for: "api-key")
let cert = try await keychain.dataWithBiometric(for: "certificate")
try keychain.contains("api-key") // => true
try keychain.delete("api-key")
try keychain.deleteAll()
let keys = try keychain.allKeys() // => ["credentials", "secure-creds"]
Keychain| Method | Description |
|---|---|
Keychain(service:) | Create a Keychain scoped to a service identifier |
.set(_:for:access:) | Store a String, Data, Bool, or Codable value |
.string(for:) | Retrieve a string |
.data(for:) | Retrieve raw data |
.object(for:as:) | Retrieve a Codable value |
.bool(for:) | Retrieve a boolean |
.contains(_:) | Check if a key exists |
.delete(_:) | Delete a single key |
.deleteAll() | Delete all items for this service |
.allKeys() | List all stored keys |
.set(_:for:expiresIn:access:) | Store a value with time-to-live |
.isExpired(_:) | Check if a stored item has expired |
.cleanExpired() | Remove all expired items, return count |
.setWithBiometric(_:for:policy:prompt:) | Store with biometric protection |
.objectWithBiometric(for:as:prompt:) | Retrieve with biometric auth (async) |
.stringWithBiometric(for:prompt:) | Retrieve biometric-protected string (async) |
.dataWithBiometric(for:prompt:) | Retrieve biometric-protected data (async) |
KeyRotation| Method | Description |
|---|---|
.rotate(in:from:to:) | Move a value from one key to another |
.rotateAll(in:matchingPrefix:transform:) | Rotate all keys matching a prefix |
AccessLevel| Value | Description |
|---|---|
.whenUnlocked | Accessible when device is unlocked (default) |
.afterFirstUnlock | Accessible after first unlock since boot |
.whenPasscodeSet | Only when device has a passcode |
BiometricPolicy| Value | Description |
|---|---|
.devicePasscode | Require device passcode |
.biometricAny | Require Face ID or Touch ID |
.biometricCurrentSet | Require currently enrolled biometric |
KeychainError| Case | Description |
|---|---|
.itemNotFound | Requested item not found |
.duplicateItem | Item already exists |
.authenticationFailed | Biometric or passcode auth failed |
.encodingFailed(String) | Failed to encode value |
.decodingFailed(String) | Failed to decode value |
.accessDenied | Access denied |
.unexpectedStatus(Int32) | Unexpected Security framework status |
swift build
swift test
If you find this project useful: