A lightweight Swift package for storing, reading, and deleting strings in the iOS and macOS keychain.
- Swift 6.0+
- iOS 13+ / macOS 10.15+
Add Vault to your project via Swift Package Manager:
dependencies: [
.package(url: "https://github.com/SparrowTek/Vault.git", from: "1.0.0")
]Vault is an actor, so all calls require await.
import Vault
let vault = Vault(configuration: KeychainConfiguration(
serviceName: "com.myapp.service",
accessGroup: nil,
accountName: "userToken"
))
// Save
try await vault.save("my-secret-value")
// Read
let value = try await vault.read()
// Delete
try await vault.delete()let vault = Vault()
let config = KeychainConfiguration(
serviceName: "com.myapp.service",
accessGroup: nil,
accountName: "userToken"
)
await vault.configure(config)
try await vault.save("my-secret-value")KeychainConfiguration controls how the keychain protects an item. accessibility
picks the kSecAttrAccessible* class (default .whenUnlocked, the system default);
accessControl adds a user-presence gate (biometry / passcode) that the keychain
evaluates on every read.
For key material — wallet seeds, private keys — use a ThisDeviceOnly class so the
item is excluded from backups and can never be restored onto a different device:
let seedConfig = KeychainConfiguration(
serviceName: "com.myapp.wallet",
accessGroup: nil,
accountName: "seed",
accessibility: .afterFirstUnlockThisDeviceOnly
)
// Require Face ID / Touch ID (or passcode) on every read:
let gatedConfig = KeychainConfiguration(
serviceName: "com.myapp.wallet",
accessGroup: nil,
accountName: "seed",
accessibility: .whenUnlockedThisDeviceOnly,
accessControl: .userPresence
)Saving over an existing item migrates its protection class to the configured one (except for access-controlled items, which would require re-authentication to modify).
accounts() lists every account stored under the configuration's service, and
deleteAll() wipes them all — useful for a complete "sign out" / "reset" flow:
let stored = try await vault.accounts()
try await vault.deleteAll()You can pass a different configuration to any individual call, which takes precedence over the stored configuration:
let otherConfig = KeychainConfiguration(
serviceName: "com.myapp.service",
accessGroup: nil,
accountName: "refreshToken"
)
try await vault.save("another-secret", configuration: otherConfig)
let token = try await vault.read(configuration: otherConfig)All errors are thrown as VaultError:
do {
let value = try await vault.read()
} catch let error as VaultError {
switch error {
case .noConfiguration:
// No KeychainConfiguration was provided
case .itemNotFound:
// No matching item exists in the keychain
case .unexpectedData:
// The keychain returned data in an unexpected format
case .encodingFailed:
// The value could not be encoded as UTF-8
case .accessControlCreationFailed:
// The requested access control could not be constructed
case .keychainFailure(let status):
// A keychain operation failed with the given OSStatus
}
}