Store NSDictionary in keychain

13,242

Solution 1

Encoding : [dic description]
Decoding : [dic propertyList]

Solution 2

You must properly serialize the NSDictionary before storing it into the Keychain. Using:

[dic description]
[dic propertyList]

you will end up with a NSDictionary collection of only NSString objects. If you want to maintain the data types of the objects, you can use NSPropertyListSerialization.

KeychainItemWrapper *keychain = [[KeychainItemWrapper alloc] initWithIdentifier:@"arbitraryId" accessGroup:nil]
NSString *error;
//The following NSData object may be stored in the Keychain
NSData *dictionaryRep = [NSPropertyListSerialization dataFromPropertyList:dictionary format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
[keychain setObject:dictionaryRep forKey:kSecValueData];

//When the NSData object object is retrieved from the Keychain, you convert it back to NSDictionary type
dictionaryRep = [keychain objectForKey:kSecValueData];
NSDictionary *dictionary = [NSPropertyListSerialization propertyListFromData:dictionaryRep mutabilityOption:NSPropertyListImmutable format:nil errorDescription:&error];

if (error) {
    NSLog(@"%@", error);
}

The NSDictionary returned by the second call to NSPropertyListSerialization will maintain original data types within the NSDictionary collection.

Solution 3

Using the KeychainItemWrapper dependency requires modifying the library/sample code to accept NSData as the encrypted payload, which is not future proof. Also, doing the NSDictionary > NSData > NSString conversion sequence just so that you can use KeychainItemWrapper is inefficient: KeychainItemWrapper will convert your string back to NSData anyway, to encrypt it.

Here's a complete solution that solves the above by utilizing the keychain library directly. It is implemented as a category so you use it like this:

// to store your dictionary
[myDict storeToKeychainWithKey:@"myStorageKey"];

// to retrieve it
NSDictionary *myDict = [NSDictionary dictionaryFromKeychainWithKey:@"myStorageKey"];

// to delete it
[myDict deleteFromKeychainWithKey:@"myStorageKey"];


and here's the Category:

@implementation NSDictionary (Keychain)

-(void) storeToKeychainWithKey:(NSString *)aKey {
    // serialize dict
    NSString *error;
    NSData *serializedDictionary = [NSPropertyListSerialization dataFromPropertyList:self format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];

    // encrypt in keychain
    if(!error) {
        // first, delete potential existing entries with this key (it won't auto update)
        [self deleteFromKeychainWithKey:aKey];

        // setup keychain storage properties
        NSDictionary *storageQuery = @{
            (id)kSecAttrAccount:    aKey,
            (id)kSecValueData:      serializedDictionary,
            (id)kSecClass:          (id)kSecClassGenericPassword,
            (id)kSecAttrAccessible: (id)kSecAttrAccessibleWhenUnlocked
        };
        OSStatus osStatus = SecItemAdd((CFDictionaryRef)storageQuery, nil);
        if(osStatus != noErr) {
            // do someting with error
        }
    }
}


+(NSDictionary *) dictionaryFromKeychainWithKey:(NSString *)aKey {
    // setup keychain query properties
    NSDictionary *readQuery = @{
        (id)kSecAttrAccount: aKey,
        (id)kSecReturnData: (id)kCFBooleanTrue,
        (id)kSecClass:      (id)kSecClassGenericPassword
    };

    NSData *serializedDictionary = nil;
    OSStatus osStatus = SecItemCopyMatching((CFDictionaryRef)readQuery, (CFTypeRef *)&serializedDictionary);
    if(osStatus == noErr) {
        // deserialize dictionary
        NSString *error;
        NSDictionary *storedDictionary = [NSPropertyListSerialization propertyListFromData:serializedDictionary mutabilityOption:NSPropertyListImmutable format:nil errorDescription:&error];
        if(error) {
            NSLog(@"%@", error);
        }
        return storedDictionary;
    }
    else {
        // do something with error
        return nil;
    }
}


-(void) deleteFromKeychainWithKey:(NSString *)aKey {
    // setup keychain query properties
    NSDictionary *deletableItemsQuery = @{
        (id)kSecAttrAccount:        aKey,
        (id)kSecClass:              (id)kSecClassGenericPassword,
        (id)kSecMatchLimit:         (id)kSecMatchLimitAll,
        (id)kSecReturnAttributes:   (id)kCFBooleanTrue
    };

    NSArray *itemList = nil;
    OSStatus osStatus = SecItemCopyMatching((CFDictionaryRef)deletableItemsQuery, (CFTypeRef *)&itemList);
    // each item in the array is a dictionary
    for (NSDictionary *item in itemList) {
        NSMutableDictionary *deleteQuery = [item mutableCopy];
        [deleteQuery setValue:(id)kSecClassGenericPassword forKey:(id)kSecClass];
        // do delete
        osStatus = SecItemDelete((CFDictionaryRef)deleteQuery);
        if(osStatus != noErr) {
            // do something with error
        }
        [deleteQuery release];
    }
}


@end

In fact, you can modify it easily to store any kind of serializable object in the keychain, not just a dictionary. Just make an NSData representation of the object you want to store.

Solution 4

Made few minor changes to Dts category. Converted to ARC and using NSKeyedArchiver to store custom objects.

@implementation NSDictionary (Keychain)

-(void) storeToKeychainWithKey:(NSString *)aKey {
    // serialize dict
    NSData *serializedDictionary = [NSKeyedArchiver archivedDataWithRootObject:self];
    // encrypt in keychain
        // first, delete potential existing entries with this key (it won't auto update)
        [self deleteFromKeychainWithKey:aKey];

        // setup keychain storage properties
        NSDictionary *storageQuery = @{
                                       (__bridge id)kSecAttrAccount:    aKey,
                                       (__bridge id)kSecValueData:      serializedDictionary,
                                       (__bridge id)kSecClass:          (__bridge id)kSecClassGenericPassword,
                                       (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked
                                       };
        OSStatus osStatus = SecItemAdd((__bridge CFDictionaryRef)storageQuery, nil);
        if(osStatus != noErr) {
            // do someting with error
        }
}


+(NSDictionary *) dictionaryFromKeychainWithKey:(NSString *)aKey {
    // setup keychain query properties
    NSDictionary *readQuery = @{
                                (__bridge id)kSecAttrAccount: aKey,
                                (__bridge id)kSecReturnData: (id)kCFBooleanTrue,
                                (__bridge id)kSecClass:      (__bridge id)kSecClassGenericPassword
                                };

    CFDataRef serializedDictionary = NULL;
    OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef)readQuery, (CFTypeRef *)&serializedDictionary);
    if(osStatus == noErr) {
        // deserialize dictionary
        NSData *data = (__bridge NSData *)serializedDictionary;
        NSDictionary *storedDictionary = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        return storedDictionary;
    }
    else {
        // do something with error
        return nil;
    }
}


-(void) deleteFromKeychainWithKey:(NSString *)aKey {
    // setup keychain query properties
    NSDictionary *deletableItemsQuery = @{
                                          (__bridge id)kSecAttrAccount:        aKey,
                                          (__bridge id)kSecClass:              (__bridge id)kSecClassGenericPassword,
                                          (__bridge id)kSecMatchLimit:         (__bridge id)kSecMatchLimitAll,
                                          (__bridge id)kSecReturnAttributes:   (id)kCFBooleanTrue
                                          };

    CFArrayRef itemList = nil;
    OSStatus osStatus = SecItemCopyMatching((__bridge CFDictionaryRef)deletableItemsQuery, (CFTypeRef *)&itemList);
    // each item in the array is a dictionary
    NSArray *itemListArray = (__bridge NSArray *)itemList;
    for (NSDictionary *item in itemListArray) {
        NSMutableDictionary *deleteQuery = [item mutableCopy];
        [deleteQuery setValue:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
        // do delete
        osStatus = SecItemDelete((__bridge CFDictionaryRef)deleteQuery);
        if(osStatus != noErr) {
            // do something with error
        }
    }
}

@end

Solution 5

You can store anything, you just need to serialize it.

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dictionary];

You should be able to store that data in the keychain.

Share:
13,242
malinois
Author by

malinois

Iphone, [apple-push-notifications], C#, Sql Server developer.@michaeldardol

Updated on June 04, 2022

Comments

  • malinois
    malinois almost 2 years

    It is possible to store a NSDictionary in the iPhone keychain, using KeychainItemWrapper (or without)? If it's not possible, have you another solution?

  • malinois
    malinois about 12 years
    *** Assertion failure in -[KeychainItemWrapper writeToKeychain] 'Couldn't add the Keychain Item.'
  • wbyoung
    wbyoung about 12 years
    You'll have to provide more details, then. There could be many reasons for 'Couldn't add the Keychain Item.'
  • Bret Deasy
    Bret Deasy over 11 years
    I edited the code to reflect more accurately how this is used with KeychainItemWrapper.
  • Rob Napier
    Rob Napier about 11 years
    This stores the data in kSecAttrService, which is not an encrypted field. I believe you meant to use kSecValueData here, which is the encrypted payload.
  • user798719
    user798719 over 10 years
    Your code does not work in ios7 for some reason. Would consider updating it to be more clear. For example, you say that we need to use [dic description] but in your example there is no dic variable.
  • Bret Deasy
    Bret Deasy over 10 years
    @user798719 - I'm actually saying not to use [dic description] and [dic propertyList] if you want to maintain data types in the NSDictionary object.
  • DTs
    DTs over 10 years
    The code doesn't work, passing NSData for key kSecValueData breaks the KeychainItemWrapper, as internally it expects a value of NSString for this key (i.e., a password). This is because it needs to encrypt the payload of kSecValueData, and before it can do so it needs to convert it to NSData. Therefore the KeychainItemWrapper already does [payloadString dataUsingEncoding:NSUTF8StringEncoding] internally, and if you pass NSData as payloadString, you'll get an Unrecognized selector sent to instance exception. Check out my answer on this page for more details and a solution.
  • zaph
    zaph over 9 years
    Not all data is valid UTF-8 so this will not work. The best option is to encode to Base64.
  • Graham Perks
    Graham Perks over 9 years
    It might work; after all the XML starts out by claiming UTF-8 encoding, <?xml version="1.0" encoding="UTF-8"?>. I believe that Apple encodes data as Base64 in the XML (See developer.apple.com/library/mac/documentation/Cocoa/Conceptu‌​al/… for an example). If that does fail, your fallback to Base64 is a good idea.
  • Fervus
    Fervus about 9 years
    Looks good. I used yours except I made deleteFromKeychainWithKey a class method so I could also perform general cleanup without having the dictionary.
  • dogsgod
    dogsgod over 8 years
    Works like a charm. I added the best parts from the KeychainItemWrapper.
  • Ne AS
    Ne AS about 7 years
    Please how can I call dictionaryFromKeychainWithKey?