I've been getting some odd reports for my app where the application settings stored into the NSUserDefaults is being cleared. The reports have all been on iOS 7s
I know you can manually clear the NSUserDefaults by either uninstalling or making a call to
NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];
But are there any other known causes for an app to clear its settings ?
If you don't want to get your data deleted, then you should use KeyChain to store values. A good way to get started: Using KeyChain
Below I am providing an example code how to store and get data back from the KeyChain
Importing required frameworks
#import <Security/Security.h>
Storing values to KeyChain
NSString *key = #"Full Name";
NSString *value = #"Steve Jobs";
NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *secItem = #{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService : service,
(__bridge id)kSecAttrAccount : key,
(__bridge id)kSecValueData : valueData,
};
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
if (status == errSecSuccess)
{
NSLog(#"Successfully stored the value");
}
else{
NSLog(#"Failed to store the value with code: %ld", (long)status);
}
Getting values back from KeyChain
NSString *keyToSearchFor = #"Full Name";
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = #{
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, (__bridge id)kSecAttrService : service,(__bridge id)kSecAttrAccount : keyToSearchFor,
(__bridge id)kSecReturnAttributes : (__bridge id)kCFBooleanTrue, };
CFDictionaryRef valueAttributes = NULL;
OSStatus results = SecItemCopyMatching((__bridge CFDictionaryRef)query,
(CFTypeRef *)&valueAttributes);
NSDictionary *attributes =
(__bridge_transfer NSDictionary *)valueAttributes;
if (results == errSecSuccess){
NSString *key, *accessGroup, *creationDate, *modifiedDate, *service;
key = attributes[(__bridge id)kSecAttrAccount];
accessGroup = attributes[(__bridge id)kSecAttrAccessGroup]; creationDate = attributes[(__bridge id)kSecAttrCreationDate]; modifiedDate = attributes[(__bridge id)kSecAttrModificationDate]; service = attributes[(__bridge id)kSecAttrService];
NSLog(#"Key = %#\n \ Access Group = %#\n \
Creation Date = %#\n \
Modification Date = %#\n \
Service = %#", key, accessGroup, creationDate, modifiedDate, service);
}
else
{
NSLog(#"Error happened with code: %ld", (long)results);
}
Related
I'm finding that sometimes I get that error when trying to get a secure item I previously succesfully stored by using Keychain API. I have found it when the app running in my device was in background state and I locked the screen. The device hasn't lock code set, and this is the function I'm calling:
+ (NSString *)findValueForKey:(NSString *)keyStr
{
NSString *valueStr = #"";
if ((keyStr != nil) && (![keyStr isEqualToString:#""])) {
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *query = #{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : service,
(__bridge id)kSecAttrAccount : keyStr,
(__bridge id)kSecReturnData : (__bridge id)kCFBooleanTrue};
CFDataRef cfValue = NULL;
OSStatus results = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&cfValue);
if ([self checkIfNoError:results]) {
valueStr = [[NSString alloc] initWithData:(__bridge_transfer NSData *)cfValue encoding:NSUTF8StringEncoding];
}
else {
NSLog(#"%#", [self getErrorMessageForStatus:results]);
}
}
return valueStr;
}
and the item was stored by calling this method:
+ (BOOL)storeInKeychainWithKey:(NSString *)keyStr withValueStr:(NSString *)valueStr
{
if ((keyStr != nil) && (![keyStr isEqualToString:#""]) &&
(valueStr != nil) && (![valueStr isEqualToString:#""])) {
NSData *valueData = [valueStr dataUsingEncoding:NSUTF8StringEncoding];
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *secItem = #{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : service,
(__bridge id)kSecAttrAccount : keyStr,
(__bridge id)kSecValueData : valueData};
CFTypeRef result = NULL;
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
NSLog(#"%#", [self getErrorMessageForStatus:status]);
return [self checkIfItem:status];
}
else {
return NO;
}
}
I thought that keychain items were always accessible in iOS... this post seems to be about something similar but I'm not sure if it is is deprecated and how should I solve this...
Thanks in advance
We had the same issue, #AppsDev, and the post you mention is accurate. We solved it by keeping the keychain as a last resort for things we need available even if we uninstall / reinstall the app.
We now loop back on the app defaults (in Swift 3 that would be UserDefaults.standard) to keep that info handy while the install lifecycle doesn't reach the "uninstalled" stage.
If uninstalled, next time is installed we go to the keychain (by definition, a just installed app isn't in background so it won't fail). With the data retrieved we refresh the app defaults and, from there, we just use the app defaults.
I don't find the information regarding such error code when calling SecItemAdd, what could be causing it?
Thanks in advance
EDIT: This is the function where I get the error:
+ (BOOL)storeWithKey:(NSString *)keyStr withValueStr:(NSString *)valueStr
{
if ((keyStr != nil) && (![keyStr isEqualToString:#""]) &&
(valueStr != nil) && (![valueStr isEqualToString:#""])) {
NSData *valueData = [valueStr dataUsingEncoding:NSUTF8StringEncoding];
NSString *service = [[NSBundle mainBundle] bundleIdentifier];
NSDictionary *secItem = #{(__bridge id)kSecClass : (__bridge id)kSecClassInternetPassword,
(__bridge id)kSecAttrService : service,
(__bridge id)kSecAttrAccount : keyStr,
(__bridge id)kSecValueData : valueData};
CFTypeRef result = NULL;
// Store value and get the result code
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)secItem, &result);
NSLog(#"'writeToKeychain'. %#", [self getErrorMessage:status]);
return [self checkIfInKeychain:status];
}
else {
return NO;
}
}
-50 means One or more parameters passed to a function were not valid. You're wrong combination of parameters.
If you use kSecAttrService and kSecAttrAccount, kSecClass should be kSecClassGenericPassword.
NSDictionary *secItem = #{(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService : service,
(__bridge id)kSecAttrAccount : keyStr,
(__bridge id)kSecValueData : valueData};
If you use kSecClassInternetPassword as kSecClass, you should use kSecAttrServer and kSecAttrPort (If needed) instead kSecAttrService.
NSDictionary *secItem = #{(__bridge id)kSecClass : (__bridge id)kSecClassInternetPassword,
(__bridge id)kSecAttrServer : #"example.com",
(__bridge id)kSecAttrPort : #(80), // Optional
(__bridge id)kSecAttrAccount : keyStr,
(__bridge id)kSecValueData : valueData};
The documentation for the error codes are in the Security/SecBase.h file. You can find them at the end.
You can also find them referenced by the documentation to SecItemAdd. https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/#//apple_ref/doc/uid/TP30000898-CH5g-CJBEABHG
i am newbie in iOS Development and i want to store my Application UUID in KeyChain so For any Time my Application UUID remaining same i do R&D on it and Find a code from this Site i mess StackOver Flow the Code is like as
+(NSUUID *)persistentIdentifierForVendor
{
static NSString * const kKeyChainVendorID = #"co.cwbrn.PersistentIdentifier";
static NSString * const kKeyChainVendorIDAccessGroup = #"<AppIdentifier>.<keychain-access-group-identifier>";
// First, check NSUserDefaults so that we're not hitting the KeyChain every single time
NSString *uuidString = [[NSUserDefaults standardUserDefaults] stringForKey:kKeyChainVendorIDGroup];
BOOL vendorIDMissingFromUserDefaults = (uuidString == nil || uuidString.length == 0);
if (vendorIDMissingFromUserDefaults) {
// Check to see if a UUID is stored in the KeyChain
NSDictionary *query = #{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount: kKeyChainVendorID,
(__bridge id)kSecAttrService: kKeyChainVendorID,
(__bridge id)kSecAttrAccessGroup: kKeyChainVendorIDAccessGroup,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne,
(__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue
};
CFTypeRef attributesRef = NULL;
OSStatus result = SecItemCopyMatching((__bridge CFDictionaryRef)query, &attributesRef);
if (result == noErr) {
// There is a UUID, so try to retrieve it
NSDictionary *attributes = (__bridge_transfer NSDictionary *)attributesRef;
NSMutableDictionary *valueQuery = [NSMutableDictionary dictionaryWithDictionary:attributes];
[valueQuery setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
[valueQuery setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
CFTypeRef passwordDataRef = NULL;
OSStatus result = SecItemCopyMatching((__bridge CFDictionaryRef)valueQuery, &passwordDataRef);
if (result == noErr) {
NSData *passwordData = (__bridge_transfer NSData *)passwordDataRef;
uuidString = [[NSString alloc] initWithBytes:[passwordData bytes]
length:[passwordData length]
encoding:NSUTF8StringEncoding];
}
}
}
// Failed to read the UUID from the KeyChain, so create a new UUID and store it
if (uuidString == nil || uuidString.length == 0) {
// Generate the new UIID
CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
uuidString = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuidRef);
CFRelease(uuidRef);
// Now store it in the KeyChain
NSDictionary *query = #{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount: kKeyChainVendorID,
(__bridge id)kSecAttrService: kKeyChainVendorID,
(__bridge id)kSecAttrAccessGroup: kKeyChainVendorIDAccessGroup,
(__bridge id)kSecAttrLabel: #"",
(__bridge id)kSecAttrDescription: #"",
(__bridge id)kSecAttrAccessible: (__bridge id)kSecAttrAccessibleAfterFirstUnlock,
(__bridge id)kSecValueData: [uuidString dataUsingEncoding:NSUTF8StringEncoding]
};
OSStatus result = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
if (result != noErr) {
NSLog(#"ERROR: Couldn't add to the Keychain. Result = %ld; Query = %#", result, query);
return nil;
}
}
// Save UUID to NSUserDefaults so that we can avoid the KeyChain next time
if (vendorIDMissingFromUserDefaults) {
[[NSUserDefaults standardUserDefaults] setObject:uuidString forKey:kKeyChainVendorIDGroup];
}
return [[NSUUID alloc] initWithUUIDString:uuidString];
}
But i want to Know that here What is kKeyChainVendorID and kKeyChainVendorIDAccessGroup here it use is like as
static NSString * const kKeyChainVendorID = #"co.cwbrn.PersistentIdentifier";
static NSString * const kKeyChainVendorIDAccessGroup = #"<AppIdentifier>.<keychain-access-group-identifier>";
For My application how i Get two Value like as kKeyChainVendorID and kKeyChainVendorIDAccessGroup??Please Give me Solution for that and in my Xcode 5.0 Version error are Occurred in line
NSString *uuidString = [[NSUserDefaults standardUserDefaults] stringForKey:kKeyChainVendorIDGroup];
Erro is:- Replace kKeyChainVendorIDGroup with kKeyChainVendorID. Can i replace it then it is Work or not Please Give me Solution For my Both Question
Thanks in advance. and thanks to nelico that post answer in stack overflow.
Here i Post My Own Answer. I get answer From this Link
http://objectivecwithsuraj.blogspot.in/2014/01/unique-identifier-uuid-ios.html
i use FDKeyChain and Write Following Code to Save UUID In KeyChain
Just Define Two String like as
static NSString * const KeychainItem_Service = #"FDKeychain";
static NSString * const KeychainItem_UUID = #"Local";
and For Get UUID I write as
uniqueIdentifier=[self generateUUID];
-(NSString *)generateUUID {
NSString *CFUUID = nil;
if (![FDKeychain itemForKey: KeychainItem_UUID
forService: KeychainItem_Service
error: nil]) {
CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
CFUUID = (NSString *)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid));
[FDKeychain saveItem: CFUUID
forKey: KeychainItem_UUID
forService: KeychainItem_Service
error: nil];
} else {
CFUUID = [FDKeychain itemForKey: KeychainItem_UUID
forService: KeychainItem_Service
error: nil];
}
return CFUUID;
}
Hope it Help to SomeOne. and if i do some fault in my answer then please give me solution. Thanks For reply.
I'm struggling with iOS keychain and i can't seem to find any good documentation.
Anyway, I have two apps, and basically all i want to do is share some data in the keychain and keep some data private so that the other app cannot access it.
I've tried to implement the KeychainItemWrapper provided by Apple, but this is simply not working. I have no issue sharing data, but if i don't set an access group, the data is still shared. I working with a device not the simulator, which could lead to the same problems.
Here is my code
App 1 :
KeychainItemWrapper *item = [[KeychainItemWrapper alloc] initWithIdentifier:#"SharedKeyChainApp" accessGroup:nil];
[item setObject:#"MyAccount" forKey:(__bridge id)kSecAttrAccount];
[item setObject:#"SecureValue" forKey:(__bridge id)kSecValueData];
App 2 :
KeychainItemWrapper *keychain = [[KeychainItemWrapper alloc] initWithIdentifier:#"SharedKeyChainApp" accessGroup:nil];
NSString *data = [keychain objectForKey:(__bridge id)kSecValueData];
NSLog(#"data is : %#",data); //Prints "data is : SecureValue"
If I remove in project properties my keychain group in one or the other app, it won't print anything. But obviously i'm not able to share data between those two apps anymore.
Thanks
If it's a shared keychain then it's shared. All the data in it will be accessible to any other app which can access the keychain.
You could:
Create 2 keychain. One shared and one private. Sharable stuff goes in shared, private stuff goes in private.
Encrypt data you won't want to to share with others.
I'd probably go with the first. IMHO, KeychainItemWrapper is pretty poor as grab and use code. It's old code which provides little in the way of functionality. I'm attaching a quick and dirty bit of code I wrote to play with and test out using the key chain functionality without KeychainItemWrapper. In this case I was playing with items both in "app" and "Security" to create some shared and non-shared items. You can't really tell that here since it's just some test code and sharing is under Targets->Capabilities->Keychain Sharing.
- (void)viewDidLoad {
[super viewDidLoad];
// [self removeKeychainItem];
// [self addKeychainItem];
[self searchForKeychainItems];
}
- (void)searchForKeychainItems {
[self log:#"\n\n EXISTING KEYCHAIN ITEM(S)"];
NSDictionary *query = #{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll,
(__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue, // returns password
(__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, // returns rest of data
// (__bridge id)kSecAttrAccessGroup: #"AAAAAAAAAA.com.foo.Security"
// (__bridge id)kSecAttrAccessGroup: #"AAAAAAAAAA.com.foo.app"
};
OSStatus resultCode;
CFArrayRef *searchResults = nil;
resultCode = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&searchResults);
NSArray *foo = CFBridgingRelease(searchResults);
[self log:[NSString stringWithFormat:#"Search result code: %d", (int)resultCode]];
[self log:[NSString stringWithFormat:#"Search Results: %#", foo]];
NSDictionary *keychainItem = foo[0];
NSString *password = [[NSString alloc] initWithData:[keychainItem objectForKey:(__bridge id)kSecValueData] encoding:NSUTF8StringEncoding];
[self log:[NSString stringWithFormat:#"password is `%#`", password]];
}
- (void)addKeychainItem {
[self log:#"\n\n ADDING KEYCHAIN ITEM"];
NSDictionary *genericDataDictionary = #{#"authState": #"1",
#"lastAuthDate": #"2/11/2014",
#"otherCrap": #"poo"};
NSData *encodedGenericData = [NSKeyedArchiver archivedDataWithRootObject:genericDataDictionary];
NSData *encodedPassword = [#"secret" dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *query = #{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrCreator: #"MyCom",
(__bridge id)kSecAttrComment: #"keychain tests",
(__bridge id)kSecAttrService: #"Credentials",
(__bridge id)kSecAttrAccount: #"username",
(__bridge id)kSecValueData: encodedPassword,
(__bridge id)kSecAttrGeneric: encodedGenericData,
(__bridge id)kSecAttrAccessGroup: #"AAAAAAAAAA.com.foo.Security"
};
OSStatus result;
result = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
NSLog(#"Add status code: %d", (int)result);
}
- (void)removeKeychainItem {
[self log:#"\n\n REMOVING KEYCHAIN ITEM"];
NSDictionary *query = #{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
// (__bridge id)kSecAttrCreator: #"MyCom",
// (__bridge id)kSecAttrService: #"Credentials",
(__bridge id)kSecAttrComment: #"New Keychain standards Test Item",
// (__bridge id)kSecAttrAccount: #"username",
// (__bridge id)kSecValueData: [#"password" dataUsingEncoding:NSUTF8StringEncoding],
// (__bridge id)kSecAttrGeneric: encodedGenericData
// (__bridge id)kSecAttrAccessGroup: #"AAAAAAAAAA.com.foo.Security"
};
OSStatus resultsCode;
resultsCode = SecItemDelete((__bridge CFDictionaryRef)query);
NSLog(#"Delete results code: %d", (int)resultsCode);
}
- (void)log:(NSString *)text {
self.textView.text = [[self.textView.text stringByAppendingString:text] stringByAppendingString:#"\n"];
}
One thing to be aware of is that you need to add both a private and a shared access group. The private access group should match the app's bundle ID and come before the shared one.
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)<APP BUNDLE ID></string>
<string>$(AppIdentifierPrefix)com.mybusiness.shared</string>
</array>
</dict>
If you only add a single named access group then this will be the default for all keychain items effectively sharing everything.
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