To enable keychain item sharing:
- Enable Keychain Sharing in target > Signing & Capabilities. Click the plus sign to add the capability Keychain Sharing.
- Set a value in "Keychain Groups" for your shared keychain items. The value must start with the Bundle Seed ID, followed by an arbitrary string. A single keychain group can store multiple items.
This adds a file .entitlements, pointed by Build Settings > Code Signing Entitlements, with something like:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix).myapp.credentials</string>
</array>
</dict>
</plist>The $(AppIdentifierPrefix) expands to your Team ID, so the final string might look like PPSF6CNP8Q.myapp.credentials.
The Team ID is listed in the Member Center. You can store multiple keychain items (like API keys, tokens, passwords) within a single group. Each item in the group is identified by a unique key when you save it to the keychain.
Note: If you’re just storing items locally for a single app, you can omit a custom access group and skip this setup. But if you use an accessGroup, make sure your test targets also have matching entitlements.
Store a value as a generic password:
let account = "OpenAI-key" // or other arbitrary string
let accessGroup = "PPSF6CNP8Q.myapp.credentials"
let store = ValueKeychainStore(accountName: account, accessGroup: accessGroup)
store.set("sk-proj-78Bmxfp9zMCrOauFJXuX") // set the key
print(store.get()) // print the keyUse the ObservedValueStore to react to updates:
let accessGroup = "PPSF6CNP8Q.myapp.credentials"
let underlyingStore = ValueKeychainStore(accountName: account, accessGroup: accessGroup)
let store = await ObservedValueStore(valueStore: underlyingStore)
let observation = await store.observe { [weak self] value in
print("Value changed to \(String(describing: newValue))")
}
try await store.set("bananas")
observation.stopObserving()Specify an appropriate accessibility level:
let extraAttributes: [String: AnyObject] = [
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
try store.create("secure-value", extraAttributes: extraAttributes)Available options include:
kSecAttrAccessibleAfterFirstUnlock: Available after first unlock until device restartkSecAttrAccessibleWhenPasscodeSetThisDeviceOnly: Only available when device has a passcodekSecAttrAccessibleWhenUnlocked: Only available when device is unlockedkSecAttrAccessibleWhenUnlockedThisDeviceOnly: most secure for local-only.
Keychain operations can throw KeychainError.unexpectedStatus(status, description). Typical codes include:
- -34018 (errSecMissingEntitlement): Missing keychain access group
- -25300 (errSecItemNotFound): Reading or updating non-existent items
- -25299 (errSecDuplicateItem): When creating an item that already exists
The Keychain wrapper provides detailed error information through KeychainError:
do {
try store.set("value")
} catch KeychainError.unexpectedStatus(let status, let message) {
// handle error
}ValueKeychainStore uses an internal lock, so get() and set() can be called from multiple threads safely.
Observers are stored in a threadsafe dictionary. However, callbacks run on a background thread, so dispatch to the main queue if UI work is needed:
_ = observedStore.observeChanges { newValue in
DispatchQueue.main.async {
// update UI
}
}When sharing keychain items between multiple apps:
- Configure the same keychain access group in both apps:
let store1 = ValueKeychainStore(
accountName: "shared-account",
accessGroup: "TEAM_ID.com.company.shared.keychain"
)
let store2 = ValueKeychainStore(
accountName: "shared-account",
accessGroup: "TEAM_ID.com.company.shared.keychain"
)- Migrate existing items if needed:
if let oldVal = try? oldStore.get() {
try? sharedStore.set(oldVal)
try? oldStore.set(nil)
}While the base implementation stores String values, you can extend ValueKeychainStore to support other types.
For instance, the code below adds Codable support:
extension ValueKeychainStore {
func set<T: Encodable>(_ value: T?, accountName: String, accessGroup: String? = nil) throws {
if let value = value {
let data = try JSONEncoder().encode(value)
let string = String(data: data, encoding: .utf8)
try set(string)
} else {
try set(nil)
}
}
func get<T: Decodable>(accountName: String, accessGroup: String? = nil) throws -> T? {
guard let string = try get(),
let data = string.data(using: .utf8) else {
return nil
}
return try JSONDecoder().decode(T.self, from: data)
}
}
struct User: Codable {
let id: String
let name: String
}
let store = ValueKeychainStore(accountName: "user-data", accessGroup: "TEAM_ID.myapp.credentials")
let user = User(id: "123", name: "John")
try store.set(user)
let savedUser: User? = try store.get()