iOS Keychain Services: only specific values allowed for kSecAttrGeneric Key?
Asked Answered
E

3

38

I am trying to use the KeychainWrapper class provided in this Apple sample code: https://developer.apple.com/library/content/samplecode/GenericKeychain/

In the sample app, the class has this init method that starts as:

- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
{
    if (self = [super init])
    {
        // Begin Keychain search setup. The genericPasswordQuery leverages the special user
        // defined attribute kSecAttrGeneric to distinguish itself between other generic Keychain
        // items which may be included by the same application.
        genericPasswordQuery = [[NSMutableDictionary alloc] init];

        [genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
        [genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];

In the sample app, it uses two values for the identifier string. "Password" and "Account Number". When implementing the class in my code, I used some custom identifiers and the code did not work. The call to SecItemAdd() failed. After some testing, it seems that using values other than "Password" and "Account Number" for the identifier does not work.

Does anyone know what values are allowed and/or if it is possible to have custom identifiers for your keychain items?

Electrify answered 3/2, 2011 at 20:48 Comment(2)
Related question: #11614547Sartre
FWIW, I filed a Radar with Apple about this issue with their sample code. See openradar.me/13472204 if you want to dupe it.Sartre
E
64

Okay, I found the solution in this blog post Keychain duplicate item when adding password

To sum it up, the issue is that the GenericKeychain sample app uses the value stored in kSecAttrGeneric key as the identifier for the keychain item when in fact that is not what the API uses to determine a unique keychain item. The keys you need to set with unique values are the kSecAttrAccount key and/or the kSecAttrService key.

You can rewrite the initilizer of KeychainItemWrapper so you don't need to change any other code by changing these lines:

Change:

[genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];

to:

[genericPasswordQuery setObject:identifier forKey:(id)kSecAttrAccount];

and change:

[keychainItemData setObject:identifier forKey:(id)kSecAttrGeneric];

to:

[keychainItemData setObject:identifier forKey:(id)kSecAttrAccount];

Or, you could do what I did and write a new initilizer that takes both of the identifying keys:

Edit: For people using ARC (you should be nowadays) check nycynik's answer for all the correct bridging notation

- (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;
{
    if (self = [super init])
    {
        NSAssert(account != nil || service != nil, @"Both account and service are nil.  Must specifiy at least one.");
        // Begin Keychain search setup. The genericPasswordQuery the attributes kSecAttrAccount and 
       // kSecAttrService are used as unique identifiers differentiating keychain items from one another
        genericPasswordQuery = [[NSMutableDictionary alloc] init];

        [genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];

        [genericPasswordQuery setObject:account forKey:(id)kSecAttrAccount];
        [genericPasswordQuery setObject:service forKey:(id)kSecAttrService];

        // The keychain access group attribute determines if this item can be shared
        // amongst multiple apps whose code signing entitlements contain the same keychain access group.
        if (accessGroup != nil)
        {
#if TARGET_IPHONE_SIMULATOR
            // Ignore the access group if running on the iPhone simulator.
            // 
            // Apps that are built for the simulator aren't signed, so there's no keychain access group
            // for the simulator to check. This means that all apps can see all keychain items when run
            // on the simulator.
            //
            // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
            // simulator will return -25243 (errSecNoAccessForItem).
#else            
            [genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
        }

        // Use the proper search constants, return only the attributes of the first match.
        [genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
        [genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];

        NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];

        NSMutableDictionary *outDictionary = nil;

        if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)
        {
            // Stick these default values into keychain item if nothing found.
            [self resetKeychainItem];

            //Adding the account and service identifiers to the keychain
            [keychainItemData setObject:account forKey:(id)kSecAttrAccount];
            [keychainItemData setObject:service forKey:(id)kSecAttrService];

            if (accessGroup != nil)
            {
#if TARGET_IPHONE_SIMULATOR
                // Ignore the access group if running on the iPhone simulator.
                // 
                // Apps that are built for the simulator aren't signed, so there's no keychain access group
                // for the simulator to check. This means that all apps can see all keychain items when run
                // on the simulator.
                //
                // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
                // simulator will return -25243 (errSecNoAccessForItem).
#else            
                [keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif
            }
        }
        else
        {
            // load the saved data from Keychain.
            self.keychainItemData = [self secItemFormatToDictionary:outDictionary];
        }

        [outDictionary release];
    }

    return self;
}

Hope this helps someone else out!

Electrify answered 15/2, 2011 at 19:15 Comment(8)
I know this answer is old(er), but saved my bacon tonight. Thanks for posting!Hardship
thanks for this, I made an ARC version, but could not leave it as a comment, so I made another answer.Leesaleese
Something as important as keychain access should have better APIs. Thanks for this answer!Craig
I fear something else is going on. I eliminated the problem by eliminating the duplication but then when I added the duplication back in the problem did not reappear.Hydrogen
What is the different between kSecAttrAccount and kSecAttrService keys? Why use one over the other? Use both? Docs seems a little skimpy here.Cracy
@Cracy For all intents and purposes they are the same. A given pair of values for those two keys determines a unique keychain item of type kSecClassGenericPassword. The idea, I would imagine is the account would be the account name and the service would be the service, e.g. account = wcochran, service = stackoverflow.comElectrify
In Xcode 7.3, the SecItemCopyMatching line is giving a warning: "Logical not is only applied to the left hand side of this comparison". The funny thing is, this actually originated from Apple's original code. Anyway, I think it should have been a !=.Dominations
Does anyone know the difference between kSecAttrAccount and kSecAttrGeneric and when/how should I use each one? Thanks.Lucretialucretius
L
10

Same as above, but it works for ARC. Thanks simon

- (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;

{
    if (self = [super init])
    {
        NSAssert(account != nil || service != nil, @"Both account and service are nil.  Must specifiy at least one.");
        // Begin Keychain search setup. The genericPasswordQuery the attributes kSecAttrAccount and
        // kSecAttrService are used as unique identifiers differentiating keychain items from one another
        genericPasswordQuery = [[NSMutableDictionary alloc] init];

        [genericPasswordQuery setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];

        [genericPasswordQuery setObject:account forKey:(__bridge id)kSecAttrAccount];
        [genericPasswordQuery setObject:service forKey:(__bridge id)kSecAttrService];

        // The keychain access group attribute determines if this item can be shared
        // amongst multiple apps whose code signing entitlements contain the same keychain access group.
        if (accessGroup != nil)
        {
#if TARGET_IPHONE_SIMULATOR
            // Ignore the access group if running on the iPhone simulator.
            //
            // Apps that are built for the simulator aren't signed, so there's no keychain access group
            // for the simulator to check. This means that all apps can see all keychain items when run
            // on the simulator.
            //
            // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
            // simulator will return -25243 (errSecNoAccessForItem).
#else
            [genericPasswordQuery setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
        }

        // Use the proper search constants, return only the attributes of the first match.
        [genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
        [genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnAttributes];

        NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];

        CFMutableDictionaryRef outDictionary = NULL;

        if (! SecItemCopyMatching((__bridge CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)
        {
            // Stick these default values into keychain item if nothing found.
            [self resetKeychainItem];

            //Adding the account and service identifiers to the keychain
            [keychainItemData setObject:account forKey:(__bridge id)kSecAttrAccount];
            [keychainItemData setObject:service forKey:(__bridge id)kSecAttrService];

            if (accessGroup != nil)
            {
#if TARGET_IPHONE_SIMULATOR
                // Ignore the access group if running on the iPhone simulator.
                //
                // Apps that are built for the simulator aren't signed, so there's no keychain access group
                // for the simulator to check. This means that all apps can see all keychain items when run
                // on the simulator.
                //
                // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the
                // simulator will return -25243 (errSecNoAccessForItem).
#else
                [keychainItemData setObject:accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
#endif
            }
        }
        else
        {
            // load the saved data from Keychain.
            keychainItemData = [self secItemFormatToDictionary:(__bridge NSDictionary *)outDictionary];
        }

        if(outDictionary) CFRelease(outDictionary);
    }

    return self;
}
Leesaleese answered 9/1, 2013 at 18:25 Comment(1)
I've been meaning to update my answer to support ARC, Thanks for saving me the trouble!Electrify
G
1

Simon almost fixed my issue because after changing the KeychainItemWrapper.m, I had issues getting and setting data to and from the keychain. So after adding this to the KeychainItemWrapper.m, I used this to get and store items:

KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:nil];
[keychainItem setObject:@"some value" forKey:(__bridge id)kSecAttrGeneric];
NSString *value = [keychainItem objectForKey: (__bridge id)kSecAttrGeneric];

Because [keychainItem objectForKey: (__bridge id)kSecAttrService] is returning the Account (in this example @"Identifier") which makes sense but it took me some time before I realized I needed to use kSecAttrGeneric to fetch data from the wrapper.

Gummite answered 19/12, 2012 at 11:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.