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 SecItemCopyMatching returns 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
  • kSecAttrAccessible setting 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. 1.Verify keychain access in entitlements:
  2. 2.```xml
  3. 3.<!-- MyApp.entitlements -->
  4. 4.<?xml version="1.0" encoding="UTF-8"?>
  5. 5.<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  6. 6."http://www.apple.com/DTDs/PropertyList-1.0.dtd">
  7. 7.<plist version="1.0">
  8. 8.<dict>
  9. 9.<key>keychain-access-groups</key>
  10. 10.<array>
  11. 11.<string>$(AppIdentifierPrefix)com.myapp.keychain</string>
  12. 12.</array>
  13. 13.</dict>
  14. 14.</plist>
  15. 15.`
  16. 16.Use correct accessibility attribute:
  17. 17.```swift
  18. 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. 1.Query keychain with proper error handling:
  2. 2.```swift
  3. 3.func getToken() -> String? {
  4. 4.let query: [String: Any] = [
  5. 5.kSecClass as String: kSecClassGenericPassword,
  6. 6.kSecAttrService as String: "com.myapp.service",
  7. 7.kSecAttrAccount as String: "user_token",
  8. 8.kSecReturnData as String: true,
  9. 9.kSecMatchLimit as String: kSecMatchLimitOne
  10. 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. 1.Clear keychain on fresh install detection:
  2. 2.```swift
  3. 3.func clearKeychain() {
  4. 4.let query: [String: Any] = [
  5. 5.kSecClass as String: kSecClassGenericPassword,
  6. 6.kSecAttrService as String: "com.myapp.service",
  7. 7.// Do NOT specify kSecAttrAccount to delete all items for this service
  8. 8.]
  9. 9.SecItemDelete(query as CFDictionary)
  10. 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 kSecAttrAccessibleAfterFirstUnlock for background-accessible tokens
  • Always check OSStatus return codes, do not assume success
  • Clear keychain on first launch after fresh install
  • Use SecItemCopyMatching with kSecReturnAttributes to debug stored items