Access App Identifier Prefix programmatically
Asked Answered
K

5

59

How can I access the Bundle Seed ID/Team ID/App Identifier Prefix string programmatically? (These are all the same thing as far as I can tell).

I am using the UICKeychainStore keychain wrapper to persist data across several applications. Each of these applications has a shared keychain access group in their entitlement plists, and share the same provisioning profile. By default, the keychain services use the first access group in the plist as the access group to save data to. This looks like "AS234SDG.com.myCompany.SpecificApp" when I debug UICKeychainStore. I would like to set the access group to "AS234SDG.com.myCompany.SharedStuff", but I can't seem to locate how to get the "AS234SDG" string of the access group programmatically, and would like to avoid hard-coding it if possible.

Kierstenkieselguhr answered 30/7, 2012 at 17:39 Comment(4)
I don't know if I understood you, but is it this NSString *bundleIDStr = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleIdentifier"]; you are looking for ?Huff
That will return "com.myCompany.SpecificApp" - I am looking for the "AS234SDG" prefix.Kierstenkieselguhr
oh I got what you are asking now...Huff
That's a great question. I couldn't find this either.Fewell
L
62

You can programmatically retrieve the Bundle Seed ID by looking at the access group attribute (i.e. kSecAttrAccessGroup) of an existing KeyChain item. In the code below, I look up for an existing KeyChain entry and create one if it doesn't not exist. Once I have a KeyChain entry, I extract the access group information from it and return the access group's first component separated by "." (period) as the Bundle Seed ID.

+ (NSString *)bundleSeedID {
    NSString *tempAccountName = @"bundleSeedID";
    NSDictionary *query = @{
        (__bridge NSString *)kSecClass : (__bridge NSString *)kSecClassGenericPassword,
        (__bridge NSString *)kSecAttrAccount : tempAccountName,
        (__bridge NSString *)kSecAttrService : @"",
        (__bridge NSString *)kSecReturnAttributes: (__bridge NSNumber *)kCFBooleanTrue,
    };
    CFDictionaryRef result = nil;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
    if (status == errSecItemNotFound)
        status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
    if (status != errSecSuccess) {
        return nil;
    }
    status = SecItemDelete((__bridge CFDictionaryRef)query); // remove temp item
    NSDictionary *dict = (__bridge_transfer NSDictionary *)result;
    NSString *accessGroup = dict[(__bridge NSString *)kSecAttrAccessGroup];
    NSArray *components = [accessGroup componentsSeparatedByString:@"."];
    NSString *bundleSeedID = [[components objectEnumerator] nextObject];
    return bundleSeedID;
}
Loverly answered 7/8, 2012 at 8:22 Comment(10)
Just curious, how did you figure out to use "bundleSeedID" as the kSecAttrAccount?Sabadell
@RajPara: That's just a random value I picked. You can change it to "com.acme.bundleSeedID" if you want. The point is to create an entry in KeyChain and read it back and extract the bundle seed id from the access group information.Loverly
Is this the ONLY solution for this? It works, but just curious if there's no other way.Knickerbocker
@orange80: You can open the "embedded.mobileprovision" file to extract the App ID. However, since it's encapsulated within a CMS structure and that the CMS api isn't public on iOS, you can extract plist payload with the following options: 1.) do some search-and-replace preprocessing to strip the CMS header/signature, 2.) embed OpenSSL and use its CMS api to extract the payload, 3.) read the CMS spec and build your own payload extractor yourself. Also, here's another possible solution (I don't know if it works though): https://mcmap.net/q/206634/-how-to-figure-out-the-app-id-programmatically-at-runtimeLoverly
This solution doesn't seem to work anymore - at least not with iOS 7 in the simulator.Fives
@Fives Access groups do not exist on Mac (and so are not available in the simulator). Have you tried on device?Rossini
I've seen this code work inconsistently on devices in iOS 8. Sometimes it would return the correct prefix, but frequently would return nil. I didn't dig into it too far, but switching to the alternate method below (changes to info.plist and then calling that value programmatically) works well.Abbacy
@DavidH For enterprise apps there's no guarantee that the embedded.mobileprovision embedded in the app is the one that is actually used to provision your app. There's various ways to install provisioning profiles. This gives you a way to keep using an app even after the embedded provisioning profile expires.Dowling
Thank you! Works on iOS 9 device. This seedId comes from provisioning profile that was used to build your app. You may locate provisionig profile at the path "~/Library/MobileDevice/Provisioning Profiles". Just open it in text viewer.Kraigkrait
Is anyone still using this on iOS 13? It seems to be broken by the addition of "com.apple.token" because the is being used as the default access group. Does anyone know a programmatic method that works on iOS 13?Bolduc
F
114

Info.plist can have your own information and if you write a value with $(AppIdentifierPrefix), it is replaced to the real app identifier prefix at building phase.

So, try this:

In your Info.plist, add an info about app identifier prefix.

<key>AppIdentifierPrefix</key>
<string>$(AppIdentifierPrefix)</string>

You can then retrieve it programmatically with Objective-C:

NSString *appIdentifierPrefix =
    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppIdentifierPrefix"];

and with Swift:

let appIdentifierPrefix =
    Bundle.main.infoDictionary!["AppIdentifierPrefix"] as! String

Note that appIdentifierPrefix ends with a period; e.g. AS234SDG.

Ferrol answered 25/2, 2015 at 8:57 Comment(9)
This seems the best way to do it. You can even store the full value just as it appears in the entitlements, e.g., $(AppIdentifierPrefix)com.MyCompany.MyApp, and than use that directly without any further modifications.Tichonn
Agreed this is preferred. The programmatic method marked as the answer may have been fine in 2012, but I'm seeing that code run inconsistently here in 2015 on iOS 8.Abbacy
It should be preferred way to do this. In case data protection enabled and shred keychain access group there is some problem to retrieve it using Security framework.Cichocki
This does not work for me on XCode 5.1.1. The $(AppIdentifierPrefix) just returns blank. Turning on the preprocess info.plist setting in the packaging build options makes no difference.Barde
XCode 8 beta has broken this build time substitution, at least thru beta 2Brilliancy
@Brilliancy Thank you for letting me know. I filed a radar. openradar.appspot.com/radar?id=4968408739217408Ferrol
excellent, I've also filed a bugreport and provided Apple a simple sample app to reproduce the issue. Hopeful for resolution, we really rely on this in our environmentBrilliancy
This one requires modifying Info.plist, which is not the end of the world but tedious if you have a lot of apps/app extensions. You need to remember to put them in all the app extensions instead of just the common API/model layer.Surfbird
"Note that appIdentifierPrefix ends with a period" Thank you for that note. Helped me out.Selfrespect
L
62

You can programmatically retrieve the Bundle Seed ID by looking at the access group attribute (i.e. kSecAttrAccessGroup) of an existing KeyChain item. In the code below, I look up for an existing KeyChain entry and create one if it doesn't not exist. Once I have a KeyChain entry, I extract the access group information from it and return the access group's first component separated by "." (period) as the Bundle Seed ID.

+ (NSString *)bundleSeedID {
    NSString *tempAccountName = @"bundleSeedID";
    NSDictionary *query = @{
        (__bridge NSString *)kSecClass : (__bridge NSString *)kSecClassGenericPassword,
        (__bridge NSString *)kSecAttrAccount : tempAccountName,
        (__bridge NSString *)kSecAttrService : @"",
        (__bridge NSString *)kSecReturnAttributes: (__bridge NSNumber *)kCFBooleanTrue,
    };
    CFDictionaryRef result = nil;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
    if (status == errSecItemNotFound)
        status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
    if (status != errSecSuccess) {
        return nil;
    }
    status = SecItemDelete((__bridge CFDictionaryRef)query); // remove temp item
    NSDictionary *dict = (__bridge_transfer NSDictionary *)result;
    NSString *accessGroup = dict[(__bridge NSString *)kSecAttrAccessGroup];
    NSArray *components = [accessGroup componentsSeparatedByString:@"."];
    NSString *bundleSeedID = [[components objectEnumerator] nextObject];
    return bundleSeedID;
}
Loverly answered 7/8, 2012 at 8:22 Comment(10)
Just curious, how did you figure out to use "bundleSeedID" as the kSecAttrAccount?Sabadell
@RajPara: That's just a random value I picked. You can change it to "com.acme.bundleSeedID" if you want. The point is to create an entry in KeyChain and read it back and extract the bundle seed id from the access group information.Loverly
Is this the ONLY solution for this? It works, but just curious if there's no other way.Knickerbocker
@orange80: You can open the "embedded.mobileprovision" file to extract the App ID. However, since it's encapsulated within a CMS structure and that the CMS api isn't public on iOS, you can extract plist payload with the following options: 1.) do some search-and-replace preprocessing to strip the CMS header/signature, 2.) embed OpenSSL and use its CMS api to extract the payload, 3.) read the CMS spec and build your own payload extractor yourself. Also, here's another possible solution (I don't know if it works though): https://mcmap.net/q/206634/-how-to-figure-out-the-app-id-programmatically-at-runtimeLoverly
This solution doesn't seem to work anymore - at least not with iOS 7 in the simulator.Fives
@Fives Access groups do not exist on Mac (and so are not available in the simulator). Have you tried on device?Rossini
I've seen this code work inconsistently on devices in iOS 8. Sometimes it would return the correct prefix, but frequently would return nil. I didn't dig into it too far, but switching to the alternate method below (changes to info.plist and then calling that value programmatically) works well.Abbacy
@DavidH For enterprise apps there's no guarantee that the embedded.mobileprovision embedded in the app is the one that is actually used to provision your app. There's various ways to install provisioning profiles. This gives you a way to keep using an app even after the embedded provisioning profile expires.Dowling
Thank you! Works on iOS 9 device. This seedId comes from provisioning profile that was used to build your app. You may locate provisionig profile at the path "~/Library/MobileDevice/Provisioning Profiles". Just open it in text viewer.Kraigkrait
Is anyone still using this on iOS 13? It seems to be broken by the addition of "com.apple.token" because the is being used as the default access group. Does anyone know a programmatic method that works on iOS 13?Bolduc
G
14

Here is the Swift version of @David H answer:

static func bundleSeedID() -> String? {
        let queryLoad: [String: AnyObject] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: "bundleSeedID" as AnyObject,
            kSecAttrService as String: "" as AnyObject,
            kSecReturnAttributes as String: kCFBooleanTrue
        ]

        var result : AnyObject?
        var status = withUnsafeMutablePointer(to: &result) {
            SecItemCopyMatching(queryLoad as CFDictionary, UnsafeMutablePointer($0))
        }

        if status == errSecItemNotFound {
            status = withUnsafeMutablePointer(to: &result) {
                SecItemAdd(queryLoad as CFDictionary, UnsafeMutablePointer($0))
            }
        }

        if status == noErr {
            if let resultDict = result as? [String: Any], let accessGroup = resultDict[kSecAttrAccessGroup as String] as? String {
                let components = accessGroup.components(separatedBy: ".")
                return components.first
            }else {
                return nil
            }
        } else {
            print("Error getting bundleSeedID to Keychain")
            return nil
        }
    }
Gilligan answered 13/10, 2017 at 9:49 Comment(1)
Works on Xcode 10 beta2, while the AppIdentifierPrefix from Info.plist gives me nilThermolysis
G
4

This is a good question but to achieve what you were intended to do, there could have been a solution that does not require to retrieve the Bundle Seed ID.

From this article, about the same keychain wrapper you're using:

By default it will pick the first access-group specified in your Entitlements.plist when writing and will search across all access-groups when none is specified.

The key will then be search in all groups where access is granted. So to solve your problem, you could add access group of all your bundle apps into your entitlements.plist instead of using a "shared stuff" group, put $(CFBundleIdentifier) as your first keychain group (your keychain wrapper will then write in this group) and you're all set

Gnni answered 3/4, 2014 at 15:10 Comment(0)
A
3

If you search in Xcode on your team's ID then you will see that this value is hosted in the build settings under the key DEVELOPMENT_TEAM.

You can retrieve this key by putting in your Info.plist file:

<key>DEVELOPMENT_TEAM</key>
<string>$(DEVELOPMENT_TEAM)</string>

Make sure to put this in every target's Info.plist file where you want to retrieve it using this code:

let teamID = Bundle.main.infoDictionary!["DEVELOPMENT_TEAM"] as! String

This solution will give you the team ID without the dot suffix.

The solution in https://mcmap.net/q/206562/-access-app-identifier-prefix-programmatically worked for me only to get the team ID from the main app target. It would not retrieve the team ID when doing the same for a Share Extension target.

Asymptote answered 7/9, 2022 at 9:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.