Skip to main content

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:
ModeDescriptionUse Case
SAMKeychainQuerySynchronizationModeYesItem will sync to iCloudUser credentials that should be available on all devices
SAMKeychainQuerySynchronizationModeNoItem will never syncDevice-specific data or sensitive local credentials
SAMKeychainQuerySynchronizationModeAnyQuery matches both synced and non-synced itemsFetching 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

1

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];
2

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];
3

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