My app acts like a VPN client to an IPSec VPN server (strongSwan).
I have used NEVPNManager to setup the VPN profile. The code is something like this:
#define KEY_PASSWORD #"password"
#define PASSWORD #"password"
#define VPN_SERVER #"10.1.1.1"
#define KEY_USERNAME #"username"
#define USERNAME #"myusername"
#define KEY_SHARED_SECRET #"sharedSecret"
#define SHARED_SECRET #"thisisthesecretekey"
#define LOCAL_IDENTIFIER #"myserver.com.client"
#define REMOTE_IDENTIFIER #"myserver.com.server"
-(void) setupVPNProfile {
NEVPNProtocolIKEv2 *protocol = [[NEVPNProtocolIKEv2 alloc] init];
protocol.username = USERNAME;
NSData *passwdRef = [self getData:KEY_PASSWORD];
if (passwdRef == nil) {
[self storeData:KEY_PASSWORD data:[PASSWORD dataUsingEncoding:NSUTF8StringEncoding]];
passwdRef = [self getData:PASSWORD];
NSLog(#"passwdRef: %#", [[NSString alloc] initWithData:passwdRef encoding:NSUTF8StringEncoding]);
}
protocol.passwordReference = passwdRef;
protocol.serverAddress = VPN_SERVER;
protocol.authenticationMethod = NEVPNIKEAuthenticationMethodSharedSecret;
NSData *sharedSecretRef = [self getData:KEY_SHARED_SECRET];
if (sharedSecretRef == nil) {
[self storeData:KEY_SHARED_SECRET data:[SHARED_SECRET dataUsingEncoding:NSUTF8StringEncoding]];
sharedSecretRef = [self getData:KEY_SHARED_SECRET];
NSLog(#"sharedSecretRef: %#", [[NSString alloc] initWithData:sharedSecretRef encoding:NSUTF8StringEncoding]);
}
protocol.sharedSecretReference = sharedSecretRef;
protocol.localIdentifier = LOCAL_IDENTIFIER;
protocol.remoteIdentifier = REMOTE_IDENTIFIER;
protocol.useExtendedAuthentication = YES;
protocol.disconnectOnSleep = NO;
self.manager.protocolConfiguration = protocol;
self.manager.enabled = YES;
}
#pragma mark - Keychain methods
- (void) storeData: (NSString * )key data:(NSData *)data {
NSLog(#"Store Data");
NSMutableDictionary * dict = [[NSMutableDictionary alloc] init];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];
[dict setObject:#"VPN" forKey:(__bridge id)kSecAttrService];
[dict setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
[dict setObject:data forKey:(__bridge id)kSecValueData];
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dict, NULL);
if(errSecSuccess != status) {
NSLog(#"Unable add item with key =%# error:%d",key,(int)status);
}
}
- (NSData *) getData: (NSString *)key {
NSMutableDictionary * dict = [[NSMutableDictionary alloc] init];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];
[dict setObject:#"VPN" forKey:(__bridge id)kSecAttrService];
[dict setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
[dict setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
[dict setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnPersistentRef];
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dict,&result);
if( status != errSecSuccess) {
NSLog(#"Unable to fetch item for key %# with error:%d",key,(int)status);
return nil;
}
NSData *resultData = (__bridge NSData *)result;
return resultData;
}
- (BOOL) removeData: (NSString *) key {
NSMutableDictionary * dict = [[NSMutableDictionary alloc] init];
[dict setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrGeneric];
[dict setObject:encodedKey forKey:(__bridge id)kSecAttrAccount];
[dict setObject:#"VPN" forKey:(__bridge id)kSecAttrService];
[dict setObject:(__bridge id)kSecAttrAccessibleAlwaysThisDeviceOnly forKey:(__bridge id)kSecAttrAccessible];
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dict);
if( status != errSecSuccess) {
NSLog(#"Unable to fetch item for key %# with error:%d",key,(int)status);
return NO;
}
return YES;
}
Here values like username, password server-address are stored in MACROS defined above. Per the required design, password and sharedSecret is to be stored as Persistent KeyChain items.
In this setup, how can I gracefully change values like username, password, serverAddress, sharedSecret?
What I have tried so far:
I changed the values like server address, username in the code. I
used storeData functions to overwrite the password and sharedSecret
persistent keychain items. However when I ran the app after making
these changes I wasn't able to connect to new VPN server. Please
note that I didn't make any mistake with the new values I had to
use. I double checked before updating. Please also note that I was
able to connect to VPN server when I created a .mobileconfig file
out of those new params. Its just that my VPN client app wasn't
able to connect to server anymore. It would try connecting and would
disconnect again. Uninstalling the app had no effect as well.
Setting manager.protocolConfiguration = nil. Doing this has no effect. The installed protocolConfiguration stays intact.
I do not want to remove the VPN profile by calling manager removePreferencesWithCompletionHandler:] since the user would see the VPN related pop-up again when he tries to connect to VPN server.
If anyone has done this before please help.
Thanks.
To change VPN account you should be able to simply change the VPN account parameters and then start VPNManager with the new values. You don't need to stop the currently running VPN Session and you certainly don't need to delete the profile, just call NEVPNManager.shared.locadFromPreferences, make the changes, then .saveToPreferences and startVPNTunnel as normal.
Since you're not able to get VPN tunnel to start even with the known good credentials using variables instead of macros, then you are probably passing values incorrectly from the keychain or not what you expect. I would compare every parameter using print statements, first with your current working solution that uses macros then with the non-working keychain code.
I'm working on an app for iOS 7 and am using ARC. I am using a web service so users have to log into the web service with a username and password. I'm know it's not good to just store the username and password as is so I would like to use keychain to store the username and password.
After doing some research it seems like people suggest using KeychainItemWrapper however that is not compatible with ARC. How can I store passwords using keychain in iOS 7 using ARC? Any help and guidelines would be greatly appreciated.
I use the following:
+ (void)keyChainSaveKey:(NSString *)key data:(id)data
{
NSMutableDictionary *keychainQuery = [self getKeychainQuery:key];
SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(__bridge id)kSecValueData];
SecItemAdd((__bridge CFDictionaryRef)keychainQuery, NULL);
}
+ (id)keyChainLoadKey:(NSString *)key
{
id ret = nil;
NSMutableDictionary *keychainQuery = [self getKeychainQuery:key];
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[keychainQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((__bridge CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
#try {
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
}
#catch (NSException *e) {
//NS Log(#"Unarchive of %# failed: %#", service, e);
}
#finally {}
}
if (keyData) CFRelease(keyData);
return ret;
}
+ (void)keyChainDeleteKey:(NSString *)service
{
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((__bridge CFDictionaryRef)keychainQuery);
}
//helper
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)key
{
return [NSMutableDictionary dictionaryWithObjectsAndKeys:
(__bridge id)kSecClassGenericPassword, (__bridge id)kSecClass,
key, (__bridge id)kSecAttrService,
key, (__bridge id)kSecAttrAccount,
(__bridge id)kSecAttrAccessibleAfterFirstUnlock, (__bridge id)kSecAttrAccessible,
nil];
}
You might need to
#import <CommonCrypto/CommonCryptor.h>
I don't remember where I found this code... so I can't give credit :(
I am using apple provided keychainwrapper sample code to store NSDictionary data which i get in my application authorization. I am receiving errSecParam (-50) as error code from SecItemAdd API.
Below is the code for keychainwrapper.m
#import "KeychainItemWrapper.h"
#import "SynthesizeSingleton.h"
#import <Security/Security.h>
#interface KeychainItemWrapper (PrivateMethods)
/*
The decision behind the following two methods (secItemFormatToDictionary and dictionaryToSecItemFormat) was
to encapsulate the transition between what the detail view controller was expecting (NSString *) and what the
Keychain API expects as a validly constructed container class.
*/
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;
// Updates the item in the keychain, or adds it if it doesn't exist.
- (void)writeToKeychain;
#end
#implementation KeychainItemWrapper
{
NSMutableDictionary *keychainItemData; // The actual keychain item data backing store.
NSMutableDictionary *genericPasswordQuery; // A placeholder for the generic keychain item query used to locate the item.
}
SYNTHESIZE_SINGLETON_FOR_CLASS(KeychainItemWrapper);
#pragma mark singleton implementation
+(KeychainItemWrapper *) sharedInstance
{
return [self sharedKeychainItemWrapper];
}
- (id) init
{
self = [super init];
if (self) {
// Do Nothing
}
return self;
}
- (void)createKeychainItemWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
{
// 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:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[genericPasswordQuery setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
// 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:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecMatchLimitOne];
[genericPasswordQuery setObject:[NSNumber numberWithBool:YES] 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];
// Add the generic attribute and the keychain access group.
[keychainItemData setObject:identifier forKey:(__bridge id)kSecAttrGeneric];
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);
}
- (void)setObject:(id)inObject forKey:(id)key
{
if (inObject == nil) return;
id currentObject = [keychainItemData objectForKey:key];
if (![currentObject isEqual:inObject])
{
[keychainItemData setObject:inObject forKey:key];
[self writeToKeychain];
}
}
- (id)objectForKey:(id)key
{
return [keychainItemData objectForKey:key];
}
- (void)resetKeychainItem
{
OSStatus junk = noErr;
if (!keychainItemData)
{
keychainItemData = [[NSMutableDictionary alloc] init];
}
else if (keychainItemData)
{
NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:keychainItemData];
junk = SecItemDelete((__bridge CFDictionaryRef)tempDictionary);
NSAssert( junk == noErr || junk == errSecItemNotFound, #"Problem deleting current dictionary." );
}
// Default attributes for keychain item.
[keychainItemData setObject:#"" forKey:(__bridge id)kSecAttrAccount];
[keychainItemData setObject:#"" forKey:(__bridge id)kSecAttrLabel];
[keychainItemData setObject:#"" forKey:(__bridge id)kSecAttrDescription];
// Default data for keychain item.
[keychainItemData setObject:#"" forKey:(__bridge id)kSecValueData];
// [keychainItemData setObject:[NSData data] forKey:(__bridge id)kSecValueData];
}
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
// The assumption is that this method will be called with a properly populated dictionary
// containing all the right key/value pairs for a SecItem.
// Create a dictionary to return populated with the attributes and data.
NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
// Add the Generic Password keychain item class attribute.
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// Convert the NSString to NSData to meet the requirements for the value type kSecValueData.
// This is where to store sensitive data that should be encrypted.
NSString *passwordString = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData];
[returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];
return returnDictionary;
}
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{
// The assumption is that this method will be called with a properly populated dictionary
// containing all the right key/value pairs for the UI element.
// Create a dictionary to return populated with the attributes and data.
NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
// Add the proper search key and class attribute.
[returnDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
[returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
// Acquire the password data from the attributes.
CFDataRef passwordData = NULL;
if (SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary, (CFTypeRef *)&passwordData) == noErr)
{
// Remove the search, class, and identifier key/value, we don't need them anymore.
[returnDictionary removeObjectForKey:(__bridge id)kSecReturnData];
// Add the password to the dictionary, converting from NSData to NSString.
NSString *password = [[NSString alloc] initWithBytes:[(__bridge NSData *)passwordData bytes] length:[(__bridge NSData *)passwordData length]
encoding:NSUTF8StringEncoding];
[returnDictionary setObject:password forKey:(__bridge id)kSecValueData];
}
else
{
// Don't do anything if nothing is found.
NSAssert(NO, #"Serious error, no matching item found in the keychain.\n");
}
if(passwordData) CFRelease(passwordData);
return returnDictionary;
}
- (void)writeToKeychain
{
CFDictionaryRef attributes = NULL;
NSMutableDictionary *updateItem = nil;
OSStatus result;
if (SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery, (CFTypeRef *)&attributes) == noErr)
{
// First we need the attributes from the Keychain.
updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge NSDictionary *)attributes];
// Second we need to add the appropriate search key/values.
[updateItem setObject:[genericPasswordQuery objectForKey:(__bridge id)kSecClass] forKey:(__bridge id)kSecClass];
// Lastly, we need to set up the updated attribute list being careful to remove the class.
NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainItemData];
[tempCheck removeObjectForKey:(__bridge id)kSecClass];
#if TARGET_IPHONE_SIMULATOR
// Remove 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).
//
// The access group attribute will be included in items returned by SecItemCopyMatching,
// which is why we need to remove it before updating the item.
[tempCheck removeObjectForKey:(__bridge id)kSecAttrAccessGroup];
#endif
// An implicit assumption is that you can only update a single item at a time.
result = SecItemUpdate((__bridge CFDictionaryRef)updateItem, (__bridge CFDictionaryRef)tempCheck);
NSAssert( result == noErr, #"Couldn't update the Keychain Item." );
}
else
{
// No previous item found; add the new one.
result = SecItemAdd((__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);
NSAssert( result == noErr, #"Couldn't add the Keychain Item." );
}
if(attributes) CFRelease(attributes);
}
#end
While using this..
KeychainItemWrapper *secClientIDMapping = [KeychainItemWrapper sharedInstance];
[secClientIDMapping createKeychainItemWithIdentifier:#"com.xxx.ClientID" accessGroup:nil];
NSString *error;
NSData *dictionaryRep = [NSPropertyListSerialization dataFromPropertyList:clientIDMapping format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
[secClientIDMapping setObject:dictionaryRep forKey:#"com.xxx.ClientID"];
Couldn't solve it from morning. Basically everywhere I am getting how to store a string but not a dictionary object.
As no body answered this question I am posting it myself as i found the way to it. When adding to keychain if we are passing other than NSString like NSData we need to serialize the data before adding to keychain.
Here is the answer..
NSDictionary *secTokenDic = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData];
NSString *error;
NSData *dictionaryRep = [NSPropertyListSerialization dataFromPropertyList:secTokenDic format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
[returnDictionary setObject:dictionaryRep forKey:(__bridge id)kSecValueData];
And similarly when retrieving need to deserialize the NSData. Hope this helps somebody else who is struggling with similar problem.
I have a strange problem with the KeyChainItemWrapper.
I used to create 2 of them like so:
KeychainItemWrapper *kcWrapper = [[KeychainItemWrapper alloc] initWithIdentifier:#"first" accessGroup:nil];
KeychainItemWrapper *kcWrapper1 = [[KeychainItemWrapper alloc] initWithIdentifier:#"second" accessGroup:nil];
I would then set an object like this:
[kcWrapper setObject:#"something" forKey:(__bridge id)kSecValueData];
[kcWrapper1 setObject:#"something1" forKey:(__bridge id)kSecValueData];
This worked perfectly, until... nothing changed?
Now I get this error:
*** Assertion failure in -[KeychainItemWrapper writeToKeychain],
/Users/wingair/Documents/iOSProjects/tulipstempelkort-
ios/stempelkort/../KeychainItemWrapper.m:305
Line 305 is:
// No previous item found; add the new one.
result = SecItemAdd((__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);
NSAssert( result == noErr, #"Couldn't add the Keychain Item." );
Been stuck here for quite some long time, any help is appreciated.
The writeToKeyChain function:
- (void)writeToKeychain
{
CFDictionaryRef attributes = NULL;
NSMutableDictionary *updateItem = nil;
OSStatus result;
if (SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery, (CFTypeRef *)&attributes) == noErr)
{
// First we need the attributes from the Keychain.
updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge NSDictionary *)attributes];
// Second we need to add the appropriate search key/values.
[updateItem setObject:[genericPasswordQuery objectForKey:(__bridge id)kSecClass] forKey:(__bridge id)kSecClass];
// Lastly, we need to set up the updated attribute list being careful to remove the class.
NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainItemData];
[tempCheck removeObjectForKey:(__bridge id)kSecClass];
[tempCheck removeObjectForKey:(__bridge id)kSecAttrAccessGroup];
// An implicit assumption is that you can only update a single item at a time.
result = SecItemUpdate((__bridge CFDictionaryRef)updateItem, (__bridge CFDictionaryRef)tempCheck);
NSAssert( result == noErr, #"Couldn't update the Keychain Item." );
It is possible to store a NSDictionary in the iPhone keychain, using KeychainItemWrapper (or without)?
If it's not possible, have you another solution?
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.
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.
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
Encoding : [dic description]
Decoding : [dic propertyList]
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.
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);
}
}
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