Overview
This guide covers common keychain issues and how to resolve them. Always check error codes and messages when debugging keychain operations.
Important : Always use the error parameter versions of SAMKeychain methods in production code. The non-error versions silently fail, making debugging difficult.
Common Issues
Password Not Found (errSecItemNotFound)
Error Code: -25300 (errSecItemNotFound)
Symptoms:
passwordForService:account: returns nil
Error domain: com.samsoffes.samkeychain
Common Causes:
Service or account name mismatch
The most common cause - typos or inconsistent naming. // ❌ Wrong: Inconsistent naming
[SAMKeychain setPassword:@"secret" forService:@"MyApp" account:@"user"];
NSString *pw = [SAMKeychain passwordForService:@"myapp" account:@"user"]; // nil!
// ✅ Correct: Exact match required
[SAMKeychain setPassword:@"secret" forService:@"MyApp" account:@"user"];
NSString *pw = [SAMKeychain passwordForService:@"MyApp" account:@"user"]; // "secret"
Solution: Use constants for service and account names:static NSString *const kKeychainService = @"com.mycompany.MyApp";
static NSString *const kKeychainAccount = @"user";
When using access groups to share keychain data between apps, all apps must use the same group. SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"SharedService";
query.account = @"user";
query.accessGroup = @"TEAMID.com.company.SharedKeychain"; // Must match exactly
query.password = @"secret";
NSError *error = nil;
if (![query save:&error]) {
NSLog(@"Save failed: %@", error);
}
Note: Access groups don’t work in the iOS Simulator.
Synchronization mode mismatch
Items saved with synchronization enabled won’t be found when searching without it. // Saved with sync enabled
SAMKeychainQuery *saveQuery = [[SAMKeychainQuery alloc] init];
saveQuery.service = @"MyApp";
saveQuery.account = @"user";
saveQuery.password = @"secret";
saveQuery.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
[saveQuery save:nil];
// Won't find it without specifying sync mode
SAMKeychainQuery *fetchQuery = [[SAMKeychainQuery alloc] init];
fetchQuery.service = @"MyApp";
fetchQuery.account = @"user";
fetchQuery.synchronizationMode = SAMKeychainQuerySynchronizationModeAny; // Use Any or Yes
[fetchQuery fetch:nil];
App reinstallation or uninstallation
Keychain items may persist after app deletion, but this behavior varies:
iOS : Keychain items usually deleted on uninstall
iOS (TestFlight/Enterprise) : Items may persist
macOS : Items always persist after app deletion
For development: Manually delete test items:[SAMKeychain deletePasswordForService:@"MyApp" account:@"testUser"];
Save Operation Fails (SAMKeychainErrorBadArguments)
Error Code: -1001 (SAMKeychainErrorBadArguments)
Cause: Missing required parameters (service, account, or password).
// ❌ Wrong: Missing password
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.account = @"user";
// query.password not set!
NSError *error = nil;
[query save:&error]; // Returns NO, error code -1001
// ✅ Correct: All required fields set
query.password = @"myPassword";
[query save:&error]; // Success
The convenience methods on SAMKeychain validate parameters, but SAMKeychainQuery requires all fields to be set manually.
Interaction Not Allowed (errSecInteractionNotAllowed)
Error Code: -25308 (errSecInteractionNotAllowed)
Symptoms:
Save or fetch fails unexpectedly
Error occurs when device is locked
Cause: Attempting to access keychain when device is locked, but accessibility type requires unlock.
// Item was saved with kSecAttrAccessibleWhenUnlocked
[SAMKeychain setAccessibilityType:kSecAttrAccessibleWhenUnlocked];
[SAMKeychain setPassword:@"secret" forService:@"MyApp" account:@"user"];
// Later, trying to access while device is locked fails
// Background task runs while device locked:
NSString *password = [SAMKeychain passwordForService:@"MyApp" account:@"user"];
// Returns nil with errSecInteractionNotAllowed
Solution: Use kSecAttrAccessibleAfterFirstUnlock for background access:
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock];
Security Framework Errors
SAMKeychain passes through Security framework error codes. Common ones:
Error Code Constant Meaning -25300errSecItemNotFoundItem doesn’t exist in keychain -25299errSecDuplicateItemItem already exists (shouldn’t happen with SAMKeychain) -25308errSecInteractionNotAllowedDevice locked or app doesn’t have permission -25293errSecAuthFailedAuthentication failed (Touch ID/Face ID) -34018(undocumented) Known iOS keychain bug - see below
iOS Keychain Bug -34018 : This is a known iOS system bug that can occur on devices, especially during development. It’s usually transient.Workarounds:
Add keychain entitlements to your app
Retry the operation after a short delay
File a radar with Apple if persistent
See: Apple Developer Forums
Debugging Techniques
Enable Detailed Error Logging
Always use the error parameter and log details:
NSError *error = nil;
NSString *password = [SAMKeychain passwordForService:@"MyApp"
account:@"user"
error:&error];
if (!password) {
if (error) {
NSLog(@"Keychain error:");
NSLog(@" Domain: %@", error.domain);
NSLog(@" Code: %ld", (long)error.code);
NSLog(@" Description: %@", error.localizedDescription);
NSLog(@" User Info: %@", error.userInfo);
} else {
NSLog(@"Password not found, but no error returned");
}
}
List All Keychain Items
See what’s actually stored in the keychain:
// List all keychain items
NSError *error = nil;
NSArray *accounts = [SAMKeychain allAccounts:&error];
NSLog(@"Found %lu keychain items:", (unsigned long)accounts.count);
for (NSDictionary *account in accounts) {
NSLog(@" Service: %@, Account: %@",
account[@"svce"], // kSAMKeychainWhereKey
account[@"acct"]); // kSAMKeychainAccountKey
}
// List items for specific service
NSArray *serviceAccounts = [SAMKeychain accountsForService:@"MyApp" error:&error];
NSLog(@"Items for MyApp service: %@", serviceAccounts);
Verify Access Group Configuration
Access groups require proper entitlements:
// Check if access group is available
#ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
#if TARGET_IPHONE_SIMULATOR
NSLog(@"⚠️ Access groups don't work in iOS Simulator");
#else
NSLog(@"✅ Access groups available");
#endif
#else
NSLog(@"❌ Access groups not available at compile time");
#endif
Entitlements file:
<? 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)com.company.SharedKeychain </ string >
</ array >
</ dict >
</ plist >
Test Synchronization Availability
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
NSLog(@"✅ iCloud Keychain sync available");
} else {
NSLog(@"❌ iCloud Keychain sync NOT available");
}
#else
NSLog(@"❌ Synchronization not available at compile time");
#endif
Handling Specific Error Codes
Robust Error Handling Pattern
- (NSString *)retrievePasswordWithRetry {
NSError *error = nil;
NSString *password = [SAMKeychain passwordForService:@"MyApp"
account:@"user"
error:&error];
if (!password && error) {
switch (error.code) {
case errSecItemNotFound:
// Password doesn't exist - normal for first launch
NSLog(@"No saved password found");
return nil;
case errSecInteractionNotAllowed:
// Device is locked
NSLog(@"Device is locked, cannot access keychain");
return nil;
case -34018:
// Known iOS bug - retry once
NSLog(@"iOS keychain bug, retrying...");
[NSThread sleepForTimeInterval:0.1];
return [SAMKeychain passwordForService:@"MyApp" account:@"user"];
default:
// Unexpected error
NSLog(@"Unexpected keychain error: %@", error);
return nil;
}
}
return password;
}
macOS-Specific
Keychain item deletion requires workaround
macOS has a quirk where SecItemDelete may not delete items created by a different version of your app. SAMKeychain handles this automatically in SAMKeychainQuery.m:216-221: // Uses SecItemCopyMatching + SecKeychainItemDelete on macOS
// instead of just SecItemDelete
No action needed - this is handled internally.
Keychain persists after uninstall
Unlike iOS, macOS never deletes keychain items when you uninstall an app. For testing: Manually delete test items or use Keychain Access.app:
Open Keychain Access (Applications > Utilities)
Search for your service name
Delete test items manually
iOS Simulator Limitations
Access Groups : Don’t work in simulator
Touch ID/Face ID : Not available
Keychain Sync : Not available
Always test keychain functionality on real devices, especially when using access groups or synchronization.
watchOS Considerations
Limited keychain capacity
No background access while watch is locked
Use kSecAttrAccessibleAfterFirstUnlock for watch complications
Slow Keychain Operations
Keychain operations should be fast (less than 10ms), but can be slow if:
Device is encrypted and locked - Operations fail or timeout
Too many queries - Cache passwords in memory when appropriate:
// ❌ Bad: Query keychain repeatedly
- (void)someMethod {
NSString *token = [SAMKeychain passwordForService:@"API" account:@"token"];
// Use token
}
// ✅ Better: Cache in memory
@property (nonatomic, strong) NSString *cachedToken;
- (NSString *)token {
if (!_cachedToken) {
_cachedToken = [SAMKeychain passwordForService:@"API" account:@"token"];
}
return _cachedToken;
}
Synchronous calls on main thread - Keychain is synchronous, so don’t call from main thread if concerned about UI responsiveness:
// Perform keychain operations on background thread if needed
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *password = [SAMKeychain passwordForService:@"MyApp" account:@"user"];
dispatch_async(dispatch_get_main_queue(), ^{
// Update UI with password
});
});
Getting More Help
FAQ Common questions and answers
GitHub Issues Report bugs or search existing issues
When reporting issues, always include:
Error code and message
Platform and OS version (iOS 17.5, macOS 14.0, etc.)
Whether it occurs on device or simulator
Code snippet that reproduces the issue