问题
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.
回答1:
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.
来源:https://stackoverflow.com/questions/18357489/how-to-store-nsdictionary-inside-keychainwrapper