Overview
iCloud Keychain allows your app’s passwords and credentials to sync automatically across a user’s devices. SAMKeychain provides full support for this feature, giving you control over which items sync and which remain local.
Understanding Synchronization
When enabled, iCloud Keychain synchronization:
Syncs keychain items across all devices signed into the same iCloud account
Happens automatically in the background
Is end-to-end encrypted by Apple
Works on iOS 7+, macOS 10.9+, tvOS, and watchOS
Synchronization is optional and controlled on a per-item basis. You can choose which passwords sync and which stay on the device.
Checking Synchronization Availability
Before using synchronization features, check if they’re available:
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
NSLog(@"iCloud Keychain synchronization is available");
// Use synchronization features
} else {
NSLog(@"iCloud Keychain synchronization is not available");
// Fall back to local-only storage
}
#else
NSLog(@"Compiled without synchronization support");
#endif
Always check isSynchronizationAvailable at runtime, even if compiled with iOS 7+ SDK. Users may have iCloud Keychain disabled in Settings.
Synchronization Modes
SAMKeychain provides three synchronization modes:
Mode Description Use Case SAMKeychainQuerySynchronizationModeYesItem will sync to iCloud User credentials that should be available on all devices SAMKeychainQuerySynchronizationModeNoItem will never sync Device-specific data or sensitive local credentials SAMKeychainQuerySynchronizationModeAnyQuery matches both synced and non-synced items Fetching items regardless of sync status
Enabling Synchronization
Using SAMKeychainQuery
The recommended approach for controlling synchronization:
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp";
query.account = @"user@example.com";
query.password = @"myPassword";
// Enable synchronization
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
NSError *error = nil;
if ([query save:&error]) {
NSLog(@"Password saved and will sync to iCloud");
} else {
NSLog(@"Failed to save: %@", error);
}
}
#endif
Storing Non-Syncing Passwords
For device-specific credentials:
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp";
query.account = @"deviceToken";
query.passwordData = deviceTokenData;
// Disable synchronization - keep on this device only
query.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
[query save:nil];
}
#endif
Fetching Synced Items
Fetching Any Item (Synced or Not)
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.account = @"user@example.com";
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
// Match both synced and non-synced items
query.synchronizationMode = SAMKeychainQuerySynchronizationModeAny;
}
#endif
if ([query fetch:nil]) {
NSLog(@"Password: %@", query.password);
}
Fetching Only Synced Items
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
NSArray *syncedItems = [query fetchAll:nil];
NSLog(@"Found %lu synced items", (unsigned long)syncedItems.count);
#endif
Fetching Only Local Items
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
NSArray *localItems = [query fetchAll:nil];
NSLog(@"Found %lu local-only items", (unsigned long)localItems.count);
#endif
When to Use Sync vs. Non-Sync
Use synchronization for user credentials
Enable sync for passwords users expect to work on all their devices: // User login credentials - should sync
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp";
query.account = userEmail;
query.password = userPassword;
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
[query save:nil];
Disable sync for device-specific data
Keep device-specific data local: // Device token - should NOT sync
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp";
query.account = @"deviceToken";
query.passwordData = pushToken;
query.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
[query save:nil];
Disable sync for highly sensitive data
For extra security, keep sensitive data on one device: // Encryption keys - should NOT sync
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp";
query.account = @"encryptionKey";
query.passwordData = encryptionKeyData;
query.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
[query save:nil];
Sync vs. ThisDeviceOnly Accessibility
Don’t confuse synchronization with accessibility attributes:
// These are DIFFERENT concepts:
// 1. Accessibility (when the item can be accessed)
[SAMKeychain setAccessibilityType:kSecAttrAccessibleWhenUnlockedThisDeviceOnly];
// 2. Synchronization (whether the item syncs to iCloud)
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
Accessibility with “ThisDeviceOnly” : Item never syncs, regardless of sync mode
// This will NEVER sync because of accessibility type
[SAMKeychain setAccessibilityType:kSecAttrAccessibleWhenUnlockedThisDeviceOnly];
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.account = @"user";
query.password = @"password";
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes; // Has no effect!
[query save:nil];
Items with ThisDeviceOnly accessibility attributes will never sync to iCloud, even if synchronization is enabled.
Migrating Between Sync States
Converting Local Item to Synced
- (void)convertToSynced:(NSString *)account {
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if (![SAMKeychainQuery isSynchronizationAvailable]) {
return;
}
// Fetch existing local item
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.account = account;
query.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
NSError *error = nil;
if ([query fetch:&error]) {
NSString *password = query.password;
// Delete local item
[query deleteItem:nil];
// Re-save with sync enabled
SAMKeychainQuery *syncQuery = [[SAMKeychainQuery alloc] init];
syncQuery.service = @"MyApp";
syncQuery.account = account;
syncQuery.password = password;
syncQuery.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
if ([syncQuery save:&error]) {
NSLog(@"Converted to synced item");
}
}
#endif
}
Converting Synced Item to Local
- (void)convertToLocal:(NSString *)account {
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if (![SAMKeychainQuery isSynchronizationAvailable]) {
return;
}
// Fetch synced item
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.account = account;
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
if ([query fetch:nil]) {
NSString *password = query.password;
// Delete synced item
[query deleteItem:nil];
// Re-save as local only
SAMKeychainQuery *localQuery = [[SAMKeychainQuery alloc] init];
localQuery.service = @"MyApp";
localQuery.account = account;
localQuery.password = password;
localQuery.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
[localQuery save:nil];
NSLog(@"Converted to local-only item");
}
#endif
}
Common Use Cases
User Preferences: Sync Toggle
- (void)savePassword:(NSString *)password
forAccount:(NSString *)account
shouldSync:(BOOL)shouldSync {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp";
query.account = account;
query.password = password;
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable] && shouldSync) {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
} else {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
}
#endif
NSError *error = nil;
if (![query save:&error]) {
NSLog(@"Failed to save: %@", error);
}
}
// Usage
BOOL userWantsSync = [[NSUserDefaults standardUserDefaults]
boolForKey:@"syncPasswordsToiCloud"];
[self savePassword:password
forAccount:account
shouldSync:userWantsSync];
Syncing OAuth Tokens
- (void)saveOAuthTokens:(NSDictionary *)tokens forAccount:(NSString *)account {
// Access token - short-lived, can sync
NSString *accessToken = tokens[@"access_token"];
if (accessToken) {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp.oauth.access";
query.account = account;
query.password = accessToken;
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
}
#endif
[query save:nil];
}
// Refresh token - long-lived, definitely should sync
NSString *refreshToken = tokens[@"refresh_token"];
if (refreshToken) {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp.oauth.refresh";
query.account = account;
query.password = refreshToken;
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
}
#endif
[query save:nil];
}
}
App Extensions and Shared Credentials
// In your main app
- (void)saveSharedCredentials:(NSString *)password forAccount:(NSString *)account {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp.shared";
query.account = account;
query.password = password;
#ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
// Share with app extension
query.accessGroup = @"com.company.shared";
#endif
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
// Also sync to other devices
if ([SAMKeychainQuery isSynchronizationAvailable]) {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
}
#endif
[query save:nil];
}
// In your app extension
- (NSString *)fetchSharedPassword:(NSString *)account {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"com.company.MyApp.shared";
query.account = account;
#ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
query.accessGroup = @"com.company.shared";
#endif
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeAny;
}
#endif
if ([query fetch:nil]) {
return query.password;
}
return nil;
}
Audit Sync Status
- (void)auditSyncStatus {
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if (![SAMKeychainQuery isSynchronizationAvailable]) {
NSLog(@"Sync not available");
return;
}
// Get all synced items
SAMKeychainQuery *syncedQuery = [[SAMKeychainQuery alloc] init];
syncedQuery.service = @"com.company.MyApp";
syncedQuery.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
NSArray *syncedItems = [syncedQuery fetchAll:nil];
// Get all local items
SAMKeychainQuery *localQuery = [[SAMKeychainQuery alloc] init];
localQuery.service = @"com.company.MyApp";
localQuery.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
NSArray *localItems = [localQuery fetchAll:nil];
NSLog(@"Synced items: %lu", (unsigned long)syncedItems.count);
NSLog(@"Local items: %lu", (unsigned long)localItems.count);
// Log details
for (NSDictionary *item in syncedItems) {
NSLog(@" Synced: %@", item[kSAMKeychainAccountKey]);
}
for (NSDictionary *item in localItems) {
NSLog(@" Local: %@", item[kSAMKeychainAccountKey]);
}
#endif
}
Best Practices
Always Check Availability
// ✅ Good: Check before using sync features
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
}
#endif
// ❌ Bad: Assuming sync is available
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
Provide Fallback Behavior
- (void)savePasswordWithSync:(NSString *)password forAccount:(NSString *)account {
SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
query.service = @"MyApp";
query.account = account;
query.password = password;
#ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([SAMKeychainQuery isSynchronizationAvailable]) {
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
NSLog(@"Saving with iCloud sync enabled");
} else {
NSLog(@"iCloud sync not available, saving locally");
}
#else
NSLog(@"Built without sync support, saving locally");
#endif
[query save:nil];
}
Use SAMKeychainQuerySynchronizationModeAny for Fetching
// ✅ Good: Fetch regardless of sync status
query.synchronizationMode = SAMKeychainQuerySynchronizationModeAny;
[query fetch:nil];
// ❌ Bad: Might miss items with different sync status
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
[query fetch:nil]; // Won't find local-only items!
Document Sync Decisions
// Good: Clear comments about sync decisions
// Synced: User expects this to work on all devices
query.synchronizationMode = SAMKeychainQuerySynchronizationModeYes;
// Not synced: Device-specific push token
query.synchronizationMode = SAMKeychainQuerySynchronizationModeNo;
Next Steps
Advanced Queries Learn more about SAMKeychainQuery features
Managing Accounts List and manage keychain accounts
Access Groups Share credentials between apps
API Reference Complete SAMKeychainQuery documentation