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 key
Use 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()