Съхранявайте NSDictionary в ключодържател

Възможно ли е да съхранявате NSDictionary в ключодържателя iPhone, като използвате KeychainItemWrapper (или без)? Ако не е възможно, имате ли друго решение?


person malinois    schedule 30.03.2012    source източник
comment
Да, но когато чета данни, имам препратка към празен NSString.   -  person malinois    schedule 30.03.2012


Отговори (7)


Трябва правилно да сериализирате NSDictionary, преди да го съхраните в Keychain. Използвайки:

[dic description]
[dic propertyList]

ще завършите с NSDictionary колекция от само NSString обекта. Ако искате да поддържате типовете данни на обектите, можете да използвате 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);
}

NSDictionary, върнат от второто извикване на NSPropertyListSerialization, ще поддържа оригинални типове данни в колекцията NSDictionary.

person Bret Deasy    schedule 15.11.2012
comment
Редактирах кода, за да отразява по-точно как това се използва с KeychainItemWrapper. - person Bret Deasy; 16.11.2012
comment
Това съхранява данните в kSecAttrService, което не е шифровано поле. Вярвам, че искахте да използвате kSecValueData тук, което е криптираният полезен товар. - person Rob Napier; 23.04.2013
comment
Вашият код не работи в ios7 по някаква причина. Бих обмислил да го актуализирам, за да бъде по-ясно. Например, вие казвате, че трябва да използваме [dic description], но във вашия пример няма dic променлива. - person user798719; 02.09.2013
comment
@user798719 - Всъщност казвам да не използвате [dic description] и [dic propertyList], ако искате да поддържате типове данни в обекта NSDictionary. - person Bret Deasy; 12.09.2013
comment
Кодът не работи, предаването на NSData за ключ kSecValueData нарушава KeychainItemWrapper, тъй като вътрешно очаква стойност NSString за този ключ (т.е. парола). Това е така, защото трябва да шифрова полезния товар на kSecValueData и преди да може да го направи, трябва да го преобразува в NSData. Следователно KeychainItemWrapper вече прави [payloadString dataUsingEncoding:NSUTF8StringEncoding] вътрешно и ако предадете NSData като payloadString, ще получите Unrecognized selector sent to instance exception. Вижте отговора ми на тази страница за повече подробности и решение. - person DTs; 18.12.2013

Използването на зависимостта KeychainItemWrapper изисква модифициране на библиотеката/примерния код, за да приеме NSData като шифрован полезен товар, което не е доказателство за бъдещето. Освен това извършването на последователността за преобразуване NSDictionary > NSData > NSString само за да можете да използвате KeychainItemWrapper е неефективно: KeychainItemWrapper така или иначе ще преобразува вашия низ обратно в NSData, за да го шифрова.

Ето цялостно решение, което решава горното, като използва директно библиотеката на ключодържателя. Той е реализиран като категория, така че го използвате по следния начин:

// to store your dictionary
[myDict storeToKeychainWithKey:@"myStorageKey"];

// to retrieve it
NSDictionary *myDict = [NSDictionary dictionaryFromKeychainWithKey:@"myStorageKey"];

// to delete it
[myDict deleteFromKeychainWithKey:@"myStorageKey"];


и ето я категорията:

@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

Всъщност можете лесно да го модифицирате, за да съхранява всякакъв вид сериализиращ се обект в ключодържателя, а не само речник. Просто направете NSData представяне на обекта, който искате да съхраните.

person DTs    schedule 18.12.2013

Направени са няколко малки промени в категорията Dts. Преобразуван в ARC и използващ NSKeyedArchiver за съхраняване на персонализирани обекти.

@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
person amol-c    schedule 08.01.2015
comment
Изглежда добре. Използвах вашия, с изключение на това, че направих deleteFromKeychainWithKey метод на клас, за да мога да извърша общо почистване, без да имам речника. - person Fervus; 09.04.2015
comment
Работи като чар. Добавих най-добрите части от KeychainItemWrapper. - person dogsgod; 11.02.2016
comment
Моля, как мога да извикам dictionaryFromKeychainWithKey? - person Ne AS; 22.03.2017

Кодиране: [dic description]
Декодиране: [dic propertyList]

person malinois    schedule 30.03.2012

Можете да съхранявате всичко, просто трябва да го сериализирате.

NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dictionary];

Трябва да можете да съхранявате тези данни в ключодържателя.

person wbyoung    schedule 30.03.2012
comment
*** Неуспешно твърдение в -[KeychainItemWrapper writeToKeychain] „Не може да се добави ключодържател.“ - person malinois; 30.03.2012
comment
Тогава ще трябва да предоставите повече подробности. Може да има много причини за „Не може да се добави ключодържателят.“ - person wbyoung; 31.03.2012

Открих, че обвивката на ключодържателя иска само низове. Дори и NSData. Така че, за да съхраните речник, ще трябва да направите както Брет предложи, но с допълнителна стъпка за преобразуване на сериализацията на NSData в низ. Като този:

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

Прочитайки го обратно:

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);
    }
}
person Graham Perks    schedule 03.05.2013
comment
Не всички данни са валидни UTF-8, така че това няма да работи. Най-добрият вариант е да кодирате към Base64. - person zaph; 09.01.2015
comment
Може да работи; след като целият XML започва с искане за UTF-8 кодиране, ‹?xml версия=1.0 кодиране=UTF-8?›. Вярвам, че Apple кодира данните като Base64 в XML (вижте developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ за пример). Ако това не успее, връщането ви към Base64 е добра идея. - person Graham Perks; 09.01.2015

Добавих поддръжка на група за достъп и безопасност на симулатора към решението на Amols:

//
//  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
person dogsgod    schedule 11.02.2016