Introduction
The iOS Keychain persists data even after an app is uninstalled. This is by design for security tokens and passwords. However, when an app is reinstalled, it may fail to access previously stored keychain items due to changes in the provisioning profile, access group, or keychain accessibility settings. This causes "item not found" errors or access failures for data that should exist.
Symptoms
OSStatus -25300(errSecItemNotFound) when querying keychain after reinstall- Auth token was saved before uninstall but cannot be retrieved after reinstall
- Works on first install but fails after delete-and-reinstall cycle
- Keychain item exists but
SecItemCopyMatchingreturns no data - Different behavior between simulator and physical device
Example error:
``
Keychain Error: OSStatus -25300
Query: [
kSecClass: kSecClassGenericPassword,
kSecAttrService: "com.myapp.service",
kSecAttrAccount: "user_token",
kSecReturnData: true
]
Result: errSecItemNotFound
Common Causes
- App bundle identifier changed between versions
- Keychain access group not properly configured in entitlements
kSecAttrAccessiblesetting restricts access after device lock- Simulator keychain not cleared between test runs
- Team ID in access group changed (different provisioning profile)
Step-by-Step Fix
- 1.Verify keychain access in entitlements:
- 2.```xml
- 3.<!-- MyApp.entitlements -->
- 4.<?xml version="1.0" encoding="UTF-8"?>
- 5.<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
- 6."http://www.apple.com/DTDs/PropertyList-1.0.dtd">
- 7.<plist version="1.0">
- 8.<dict>
- 9.<key>keychain-access-groups</key>
- 10.<array>
- 11.<string>$(AppIdentifierPrefix)com.myapp.keychain</string>
- 12.</array>
- 13.</dict>
- 14.</plist>
- 15.
` - 16.Use correct accessibility attribute:
- 17.```swift
- 18.import Security
func saveToken(_ token: String) -> OSStatus { let data = token.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "com.myapp.service", kSecAttrAccount as String: "user_token", kSecValueData as String: data, // Use thisAfterFirstUnlock so it is available after reboot kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock ] SecItemDelete(query as CFDictionary) // Remove existing return SecItemAdd(query as CFDictionary, nil) } ```
- 1.Query keychain with proper error handling:
- 2.```swift
- 3.func getToken() -> String? {
- 4.let query: [String: Any] = [
- 5.kSecClass as String: kSecClassGenericPassword,
- 6.kSecAttrService as String: "com.myapp.service",
- 7.kSecAttrAccount as String: "user_token",
- 8.kSecReturnData as String: true,
- 9.kSecMatchLimit as String: kSecMatchLimitOne
- 10.]
var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result)
switch status { case errSecSuccess: guard let data = result as? Data, let token = String(data: data, encoding: .utf8) else { return nil } return token case errSecItemNotFound: print("Token not found in keychain - user needs to log in again") return nil case errSecAuthFailed: print("Keychain access denied - check entitlements") return nil default: print("Keychain error: \(status)") return nil } } ```
- 1.Clear keychain on fresh install detection:
- 2.```swift
- 3.func clearKeychain() {
- 4.let query: [String: Any] = [
- 5.kSecClass as String: kSecClassGenericPassword,
- 6.kSecAttrService as String: "com.myapp.service",
- 7.// Do NOT specify kSecAttrAccount to delete all items for this service
- 8.]
- 9.SecItemDelete(query as CFDictionary)
- 10.}
// Detect first launch and clear stale keychain entries func application(_ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { if !UserDefaults.standard.bool(forKey: "hasLaunchedBefore") { clearKeychain() // Clean up from previous installation UserDefaults.standard.set(true, forKey: "hasLaunchedBefore") } return true } ```
Prevention
- Document keychain access group requirements in README
- Test the uninstall-reinstall flow as part of your QA process
- Use
kSecAttrAccessibleAfterFirstUnlockfor background-accessible tokens - Always check
OSStatusreturn codes, do not assume success - Clear keychain on first launch after fresh install
- Use
SecItemCopyMatchingwithkSecReturnAttributesto debug stored items