Store NSDictionary in keychain
Asked Answered
M

7

20

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

Moriah answered 30/3, 2012 at 18:41 Comment(1)
Yes, but when I read data, I have a reference to a empty NSString.Lymphadenitis
M
6

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

Moriah answered 30/3, 2012 at 21:31 Comment(0)
B
28

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.

Billfold answered 15/11, 2012 at 16:26 Comment(5)
I edited the code to reflect more accurately how this is used with KeychainItemWrapper.Billfold
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.Jessen
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.Defiance
@Defiance - I'm actually saying not to use [dic description] and [dic propertyList] if you want to maintain data types in the NSDictionary object.Billfold
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.Mathes
M
15

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.

Mathes answered 18/12, 2013 at 4:21 Comment(0)
C
14

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
Condition answered 8/1, 2015 at 19:31 Comment(3)
Looks good. I used yours except I made deleteFromKeychainWithKey a class method so I could also perform general cleanup without having the dictionary.Lowering
Works like a charm. I added the best parts from the KeychainItemWrapper.Kazbek
Please how can I call dictionaryFromKeychainWithKey?Krasnoyarsk
M
6

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

Moriah answered 30/3, 2012 at 21:31 Comment(0)
S
2

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.

Selaginella answered 30/3, 2012 at 18:49 Comment(2)
*** Assertion failure in -[KeychainItemWrapper writeToKeychain] 'Couldn't add the Keychain Item.'Lymphadenitis
You'll have to provide more details, then. There could be many reasons for 'Couldn't add the Keychain Item.'Selaginella
S
0

I found that the keychain wrapper only wants strings. Not even NSData. So to store a dictionary you'll have to do as Bret suggested, but with an extra step to convert the NSData serialization to a string. Like this:

NSString *error;
KeychainItemWrapper *keychain = [[KeychainItemWrapper alloc] initWithIdentifier:MY_STRING accessGroup:nil];
NSData *dictionaryRep = [NSPropertyListSerialization dataFromPropertyList:dictToSave format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
NSString *xml = [[NSString alloc] initWithBytes:[dictionaryRep bytes] length:[dictionaryRep length] encoding:NSUTF8StringEncoding];
[keychain setObject:xml forKey:(__bridge id)(kSecValueData)];

Reading it back:

NSError *error;
NSString *xml = [keychain objectForKey:(__bridge id)(kSecValueData)];
if (xml && xml.length) {
    NSData *dictionaryRep = [xml dataUsingEncoding:NSUTF8StringEncoding];
    dict = [NSPropertyListSerialization propertyListWithData:dictionaryRep options:NSPropertyListImmutable format:nil error:&error];
    if (error) {
        NSLog(@"%@", error);
    }
}
Scolecite answered 3/5, 2013 at 19:55 Comment(2)
Not all data is valid UTF-8 so this will not work. The best option is to encode to Base64.Avalos
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/Conceptual/… for an example). If that does fail, your fallback to Base64 is a good idea.Scolecite
K
0

I added access group support and simulator safety to Amols solution:

//
//  NSDictionary+SharedKeyChain.h
//  LHSharedKeyChain
//

#import <Foundation/Foundation.h>

@interface NSDictionary (SharedKeyChain)

/**
 *  Returns a previously stored dictionary from the KeyChain.
 *
 *  @param  key          NSString    The name of the dictionary. There can be multiple dictionaries stored in the KeyChain.
 *  @param  accessGroup  NSString    Access group for shared KeyChains, set to nil for no group.
 *
 *  @return NSDictionary    A dictionary that has been stored in the Keychain, nil if no dictionary for the key and accessGroup exist.
 */
+ (NSDictionary *)dictionaryFromKeychainWithKey:(NSString *)key accessGroup:(NSString *)accessGroup;

/**
 *  Deletes a previously stored dictionary from the KeyChain.
 *
 *  @param  key          NSString    The name of the dictionary. There can be multiple dictionaries stored in the KeyChain.
 *  @param  accessGroup  NSString    Access group for shared KeyChains, set to nil for no group.
 */
+ (void)deleteFromKeychainWithKey:(NSString *)key accessGroup:(NSString *)accessGroup;

/**
 *  Save dictionary instance to the KeyChain. Any previously existing data with the same key and accessGroup will be overwritten.
 *
 *  @param  key          NSString    The name of the dictionary. There can be multiple dictionaries stored in the KeyChain.
 *  @param  accessGroup  NSString    Access group for shared KeyChains, set to nil for no group.
 */
- (void)storeToKeychainWithKey:(NSString *)key accessGroup:(NSString *)accessGroup;

@end

//
//  NSDictionary+SharedKeyChain.m
//  LHSharedKeyChain
//

#import "NSDictionary+SharedKeyChain.h"

@implementation NSDictionary (SharedKeyChain)

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

    // setup keychain storage properties
    NSDictionary *storageQuery = @{
        (__bridge id)kSecAttrAccount: key,
#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
        (__bridge id)kSecAttrAccessGroup: accessGroup,
#endif
        (__bridge id)kSecValueData: serializedDictionary,
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleWhenUnlocked
    };
    OSStatus status = SecItemAdd ((__bridge CFDictionaryRef)storageQuery, nil);
    if (status != noErr)
    {
        NSLog (@"%d %@", (int)status, @"Couldn't save to Keychain.");
    }
}


+ (NSDictionary *)dictionaryFromKeychainWithKey:(NSString *)key accessGroup:(NSString *)accessGroup;
{
    // setup keychain query properties
    NSDictionary *readQuery = @{
        (__bridge id)kSecAttrAccount: key,
#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
        (__bridge id)kSecAttrAccessGroup: accessGroup,
#endif
        (__bridge id)kSecReturnData: (id)kCFBooleanTrue,
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword
    };

    CFDataRef serializedDictionary = NULL;
    OSStatus status = SecItemCopyMatching ((__bridge CFDictionaryRef)readQuery, (CFTypeRef *)&serializedDictionary);
    if (status == noErr)
    {
        // deserialize dictionary
        NSData *data = (__bridge NSData *)serializedDictionary;
        NSDictionary *storedDictionary = [NSKeyedUnarchiver unarchiveObjectWithData:data];
        return storedDictionary;
    }
    else
    {
        NSLog (@"%d %@", (int)status, @"Couldn't read from Keychain.");
        return nil;
    }
}


+ (void)deleteFromKeychainWithKey:(NSString *)key accessGroup:(NSString *)accessGroup;
{
    // setup keychain query properties
    NSDictionary *deletableItemsQuery = @{
        (__bridge id)kSecAttrAccount: key,
#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
        (__bridge id)kSecAttrAccessGroup: accessGroup,
#endif
        (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
        (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll,
        (__bridge id)kSecReturnAttributes: (id)kCFBooleanTrue
    };

    CFArrayRef itemList = nil;
    OSStatus status = 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
        status = SecItemDelete ((__bridge CFDictionaryRef)deleteQuery);
        if (status != noErr)
        {
            NSLog (@"%d %@", (int)status, @"Couldn't delete from Keychain.");
        }
    }
}

@end
Kazbek answered 11/2, 2016 at 20:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.