1

我正在使用苹果提供的 keychainwrapper 示例代码来存储我在应用程序授权中获得的 NSDictionary 数据。我从 SecItemAdd API 收到 errSecParam (-50) 作为错误代码。下面是 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

在使用这个..

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"];

从早上开始无法解决。基本上我到处都在学习如何存储字符串而不是字典对象。

4

1 回答 1

4

由于没有人回答这个问题,我自己发布了它,因为我找到了解决方法。添加到钥匙串时,如果我们传递的不是 NSString (如 NSData),我们需要在添加到钥匙串之前序列化数据。

这是答案..

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];

同样在检索时需要反序列化 NSData。希望这可以帮助其他遇到类似问题的人。

于 2013-08-22T10:00:34.970 回答