Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Store NSDictionary in keychain

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

like image 344
malinois Avatar asked Mar 30 '12 18:03

malinois


2 Answers

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.

like image 72
Bret Deasy Avatar answered Oct 07 '22 19:10

Bret Deasy


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.

like image 43
DTs Avatar answered Oct 07 '22 19:10

DTs