How to migrate macOS keychain entry to new rewritten app?

I'm working on replacing an AppKit-based Mac app with one built on Catalyst, and the Catalyst app doesn't seem to be able to read the keychain item that was saved by the old app.

Both apps are using the same bundle ID. The old app uses the old SecKeychain APIs - SecKeychainFindGenericPassword and friends - and the Catalyst app uses the newer SecItemCopyMatching and such. When I try using the new API in the old app to search for the entry, it works, but the exact same code in Catalyst fails.

Here's how I save an item in the old app:

NSString *strItemId = @"my_item_id;

NSString *username = @"user";
const char *userPointer = [username UTF8String];

NSString *password = @"password";
const char *pwPointer = [password UTF8String];

SecKeychainItemRef ref = NULL;
OSStatus status = SecKeychainFindGenericPassword(0, (UInt32)strlen(strItemId.UTF8String), strItemId.UTF8String, 0, NULL, NULL, NULL, &ref);

if (status == errSecSuccess && ref != NULL)
{
    //update existing item
    SecKeychainAttribute attr;
    attr.length = (UInt32)strlen(userPointer);
    attr.data = (void *)userPointer;
    attr.tag = kSecAccountItemAttr;
    
    SecKeychainAttributeList list;
    list.count = 1;
    list.attr = &attr;
    
    OSStatus writeStatus = SecKeychainItemModifyAttributesAndData(ref, &list, (UInt32)strlen(pwPointer), pwPointer);
}
else
{
    status = SecKeychainAddGenericPassword(NULL, (UInt32)strlen(strItemId.UTF8String), strItemId.UTF8String, (UInt32)strlen(userPointer), userPointer, (UInt32)strlen(pwPointer), pwPointer, NULL);
}

And here's the query code that works in the old app but returns errSecItemNotFound in Catalyst:

NSMutableDictionary *queryDict = [[[NSMutableDictionary alloc]init]autorelease];
[queryDict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];

[queryDict setObject:(@"my_item_id") forKey:(__bridge id)kSecAttrService];

[queryDict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[queryDict setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];

CFMutableDictionaryRef outDictionary = nil;
OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)queryDict, (CFTypeRef *)&outDictionary);

I tried creating a new blank AppKit-based Mac app project in Xcode and gave it the old Mac app's bundle ID, and the SecItemCopyMatching query code above works there. Then I created a new iOS target with Catalyst enabled, also with the same bundle ID, and the query code running there under Catalyst returned errSecItemNotFound. So maybe the issue is something specific to Catalyst?

Is there something I need to do with the Catalyst app to give it access to the old app's keychain entry, besides setting its bundle ID to match the old app?

Answered by DTS Engineer in 807720022

Before you continue, read TN3137 On Mac keychain APIs and implementations. This explains:

  • The difference between the file-based keychain and the data protection keychain.

  • Clarifies that Mac Catalyst apps only have access to the data protection keychain.

I also recommend that you read:

Your code snippets could be a lot easier (-:


Unless you went out of your way to use the data protection keychain, your AppKit app will have stored its items in the file-based keychain. Your Mac Catalyst app can only access the data protection keychain, and thus can’t read those items.

There are two ways around this:

  • You could push an update to your AppKit app that moves the items to the data protection keychain.

  • You can embed a helper tool within your Mac Catalyst app that does this.

Both have drawbacks. The first option only works for users who update to the new AppKit app and then run it before then updating to the Mac Catalyst app.

The second requires you to create some subtle code:

  • The helper tool can’t be just a tool; it has to be packaged in an app-like wrapper. You need that packaging in order to provide a provisioning profile that authorises access to the keychain access group you share with the Catalyst app.

    I talk about this overall idea, albeit in a very different context, in Signing a daemon with a restricted entitlement.

  • The Mac Catalyst app can’t use Process (or NSTask) to run the helper. That’s because Mac Catalyst doesn’t allow you to access those types (r. 82912914). You have to use posix_spawn instead.

    Oh wait, I just retested this in Xcode 16.0 and it seems that we’ve fixed it. Nice! That significantly reduces the trickiness of the helper tool approach.

There’s one additional gotcha here. The helper tool must have the same designated requirement as the original AppKit app. See TN3127 Inside Code Signing: Requirements for more background on DRs. The tricky thing is that the bundle ID usually forms part of the DR. That means that the helper tool will need to have the same bundle ID as the main app. That should work, but I could imagine it causing complications. For example, if you ship this product via the App Store, it’s not clear what the app ingestion process will make of it having two programs with the same bundle ID.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

Before you continue, read TN3137 On Mac keychain APIs and implementations. This explains:

  • The difference between the file-based keychain and the data protection keychain.

  • Clarifies that Mac Catalyst apps only have access to the data protection keychain.

I also recommend that you read:

Your code snippets could be a lot easier (-:


Unless you went out of your way to use the data protection keychain, your AppKit app will have stored its items in the file-based keychain. Your Mac Catalyst app can only access the data protection keychain, and thus can’t read those items.

There are two ways around this:

  • You could push an update to your AppKit app that moves the items to the data protection keychain.

  • You can embed a helper tool within your Mac Catalyst app that does this.

Both have drawbacks. The first option only works for users who update to the new AppKit app and then run it before then updating to the Mac Catalyst app.

The second requires you to create some subtle code:

  • The helper tool can’t be just a tool; it has to be packaged in an app-like wrapper. You need that packaging in order to provide a provisioning profile that authorises access to the keychain access group you share with the Catalyst app.

    I talk about this overall idea, albeit in a very different context, in Signing a daemon with a restricted entitlement.

  • The Mac Catalyst app can’t use Process (or NSTask) to run the helper. That’s because Mac Catalyst doesn’t allow you to access those types (r. 82912914). You have to use posix_spawn instead.

    Oh wait, I just retested this in Xcode 16.0 and it seems that we’ve fixed it. Nice! That significantly reduces the trickiness of the helper tool approach.

There’s one additional gotcha here. The helper tool must have the same designated requirement as the original AppKit app. See TN3127 Inside Code Signing: Requirements for more background on DRs. The tricky thing is that the bundle ID usually forms part of the DR. That means that the helper tool will need to have the same bundle ID as the main app. That should work, but I could imagine it causing complications. For example, if you ship this product via the App Store, it’s not clear what the app ingestion process will make of it having two programs with the same bundle ID.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

How to migrate macOS keychain entry to new rewritten app?
 
 
Q