Задачи iOS Keychain Services

В этой главе описываются и иллюстрирует использование основных функций Keychain Services, которые доступны и в iOS и в OS X.

Функции, описанные в этой главе, включают Вам к:

Понятия Keychain Services обеспечивают введение в понятия и терминологию Keychain Services. Для получения дальнейшей информации обо всех функциях Keychain Services, посмотрите Ссылку Keychain Services.

Добавление служб цепочки для ключей к приложению

Большинство приложений для iOS должно использовать Keychain Services только, чтобы добавить новый пароль к цепочке для ключей, изменить существующий элемент цепочки для ключей или получить пароль при необходимости. Keychain Services обеспечивает следующие функции для выполнения этих задач:

Вы используете интернет-пароли для доступа к серверам и веб-сайтам по Интернету и универсальным паролям для любой другой защищенной паролем службы (таким как база данных или планирование приложения). В iOS Keychain Services сертификаты, ключи и идентификационные данные сохранены и получены точно таким же образом как пароли, за исключением того, что у них есть различные атрибуты.

Рисунок 2-1 показывает блок-схему того, как приложение могло бы использовать эти функции для получения доступа к интернет-Ftp-серверу.

Рисунок 2-1  , Получающий доступ к Интернет-серверу с помощью iPhone Keychain Services
FLowchart showing use of iPhone Keychain Services to store an Internet password

Пользователь приложения запускает путем выбора сервера File Transfer Protocol (FTP). Вызовы приложения SecItemCopyMatching, передача его словарь, содержащий атрибуты, идентифицирующие элемент цепочки для ключей. Если пароль находится на цепочке для ключей, функция возвращает пароль приложению, отправляющему его в Ftp-сервер для аутентификации пользователя. Если аутентификация успешно выполняется, подпрограмма закончена. Если аутентификация перестала работать, отображения приложения диалоговое окно для запроса имени пользователя и пароля.

Если пароль не находится на цепочке для ключей, то SecItemCopyMatching возвраты errSecItemNotFound код результата. В этом случае также, отображения приложения диалоговое окно для запроса имени пользователя и пароля. (Это диалоговое окно должно также включать Кнопку отмены, но тот выбор был опущен от фигуры, чтобы препятствовать блок-схеме становиться чрезмерно сложной.)

Получив пароль от пользователя, приложение продолжается для аутентификации пользователя к Ftp-серверу. Когда аутентификация успешно выполнилась, приложение может предположить, что информация, вводимая пользователем, была допустима. Приложение тогда выводит на экран другое диалоговое окно, спрашивая пользователя, сохранить ли пароль на цепочке для ключей. Если пользователь выбирает No, то подпрограмма закончена. Если пользователь выбирает Yes, то приложение вызывает SecItemAdd функция (если это - новый элемент цепочки для ключей), или SecItemUpdate функция (для обновления существующего элемента цепочки для ключей) прежде, чем закончить подпрограмму.

Перечисление 2-1 показывает, как типовое приложение могло бы использовать функции Keychain Services, чтобы получить и установить пароли для универсальных элементов. Можно получить и установить атрибуты элемента цепочки для ключей (такие как имя пользователя или имя службы) использующий эти те же функции.

Перечисление 2-1  , Добирающееся и устанавливающее пароли в iOS Keychain Services

#import <Foundation/Foundation.h>
#import <Security/Security.h>
 
//Define an Objective-C wrapper class to hold Keychain Services code.
@interface KeychainWrapper : NSObject {
    NSMutableDictionary        *keychainData;
    NSMutableDictionary        *genericPasswordQuery;
}
 
@property (nonatomic, strong) NSMutableDictionary *keychainData;
@property (nonatomic, strong) NSMutableDictionary *genericPasswordQuery;
 
- (void)mySetObject:(id)inObject forKey:(id)key;
- (id)myObjectForKey:(id)key;
- (void)resetKeychainItem;
 
@end
 
/* ********************************************************************** */
//Unique string used to identify the keychain item:
static const UInt8 kKeychainItemIdentifier[]    = "com.apple.dts.KeychainUI\0";
 
@interface KeychainWrapper (PrivateMethods)
 
 
//The following two methods translate dictionaries between the format used by
// the view controller (NSString *) and the Keychain Services API:
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;
// Method used to write data to the keychain:
- (void)writeToKeychain;
 
@end
 
@implementation KeychainWrapper
 
//Synthesize the getter and setter:
@synthesize keychainData, genericPasswordQuery;
 
- (id)init
{
    if ((self = [super init])) {
 
        OSStatus keychainErr = noErr;
        // Set up the keychain search dictionary:
        genericPasswordQuery = [[NSMutableDictionary alloc] init];
        // This keychain item is a generic password.
        [genericPasswordQuery setObject:(__bridge id)kSecClassGenericPassword
                                 forKey:(__bridge id)kSecClass];
        // The kSecAttrGeneric attribute is used to store a unique string that is used
        // to easily identify and find this keychain item. The string is first
        // converted to an NSData object:
        NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier
                                  length:strlen((const char *)kKeychainItemIdentifier)];
        [genericPasswordQuery setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric];
        // Return the attributes of the first match only:
        [genericPasswordQuery setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
        // Return the attributes of the keychain item (the password is
        //  acquired in the secItemFormatToDictionary: method):
        [genericPasswordQuery setObject:(__bridge id)kCFBooleanTrue
                                 forKey:(__bridge id)kSecReturnAttributes];
 
        //Initialize the dictionary used to hold return data from the keychain:
        CFMutableDictionaryRef outDictionary = nil;
        // If the keychain item exists, return the attributes of the item:
        keychainErr = SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery,
                                         (CFTypeRef *)&outDictionary);
        if (keychainErr == noErr) {
            // Convert the data dictionary into the format used by the view controller:
            self.keychainData = [self secItemFormatToDictionary:(__bridge_transfer NSMutableDictionary *)outDictionary];
        } else if (keychainErr == errSecItemNotFound) {
            // Put default values into the keychain if no matching
            // keychain item is found:
            [self resetKeychainItem];
            if (outDictionary) CFRelease(outDictionary);
        } else {
            // Any other error is unexpected.
            NSAssert(NO, @"Serious error.\n");
            if (outDictionary) CFRelease(outDictionary);
        }
    }
    return self;
}
 
// Implement the mySetObject:forKey method, which writes attributes to the keychain:
- (void)mySetObject:(id)inObject forKey:(id)key
{
    if (inObject == nil) return;
    id currentObject = [keychainData objectForKey:key];
    if (![currentObject isEqual:inObject])
    {
        [keychainData setObject:inObject forKey:key];
        [self writeToKeychain];
    }
}
 
// Implement the myObjectForKey: method, which reads an attribute value from a dictionary:
- (id)myObjectForKey:(id)key
{
    return [keychainData objectForKey:key];
}
 
// Reset the values in the keychain item, or create a new item if it
// doesn't already exist:
 
- (void)resetKeychainItem
{
    if (!keychainData) //Allocate the keychainData dictionary if it doesn't exist yet.
    {
        self.keychainData = [[NSMutableDictionary alloc] init];
    }
    else if (keychainData)
    {
        // Format the data in the keychainData dictionary into the format needed for a query
        //  and put it into tmpDictionary:
        NSMutableDictionary *tmpDictionary =
                            [self dictionaryToSecItemFormat:keychainData];
        // Delete the keychain item in preparation for resetting the values:
        OSStatus errorcode = SecItemDelete((__bridge CFDictionaryRef)tmpDictionary);
        NSAssert(errorcode == noErr, @"Problem deleting current keychain item." );
    }
 
    // Default generic data for Keychain Item:
    [keychainData setObject:@"Item label" forKey:(__bridge id)kSecAttrLabel];
    [keychainData setObject:@"Item description" forKey:(__bridge id)kSecAttrDescription];
    [keychainData setObject:@"Account" forKey:(__bridge id)kSecAttrAccount];
    [keychainData setObject:@"Service" forKey:(__bridge id)kSecAttrService];
    [keychainData setObject:@"Your comment here." forKey:(__bridge id)kSecAttrComment];
    [keychainData setObject:@"password" forKey:(__bridge id)kSecValueData];
}
 
// Implement the dictionaryToSecItemFormat: method, which takes the attributes that
// you want to add to the keychain item and sets up a dictionary in the format
// needed by Keychain Services:
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{
    // This method must be called with a properly populated dictionary
    // containing all the right key/value pairs for a keychain item search.
 
    // Create the return dictionary:
    NSMutableDictionary *returnDictionary =
                   [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];
 
    // Add the keychain item class and the generic attribute:
    NSData *keychainItemID = [NSData dataWithBytes:kKeychainItemIdentifier
                              length:strlen((const char *)kKeychainItemIdentifier)];
    [returnDictionary setObject:keychainItemID forKey:(__bridge id)kSecAttrGeneric];
    [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
 
    // Convert the password NSString to NSData to fit the API paradigm:
    NSString *passwordString = [dictionaryToConvert objectForKey:(__bridge id)kSecValueData];
    [returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding]
                                                           forKey:(__bridge id)kSecValueData];
    return returnDictionary;
}
 
// Implement the secItemFormatToDictionary: method, which takes the attribute dictionary
//  obtained from the keychain item, acquires the password from the keychain, and
//  adds it to the attribute dictionary:
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{
    // This method must be called with a properly populated dictionary
    // containing all the right key/value pairs for the keychain item.
 
    // Create a return dictionary populated with the attributes:
    NSMutableDictionary *returnDictionary = [NSMutableDictionary
                                          dictionaryWithDictionary:dictionaryToConvert];
 
    // To acquire the password data from the keychain item,
    // first add the search key and class attribute required to obtain the password:
    [returnDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
    [returnDictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
    // Then call Keychain Services to get the password:
    CFDataRef passwordData = NULL;
    OSStatus keychainError = noErr; //
    keychainError = SecItemCopyMatching((__bridge CFDictionaryRef)returnDictionary,
                                       (CFTypeRef *)&passwordData);
    if (keychainError == noErr)
    {
        // Remove the kSecReturnData key; we don't need it anymore:
        [returnDictionary removeObjectForKey:(__bridge id)kSecReturnData];
 
        // Convert the password to an NSString and add it to the return dictionary:
        NSString *password = [[NSString alloc] initWithBytes:[(__bridge_transfer NSData *)passwordData bytes]
                 length:[(__bridge NSData *)passwordData length] encoding:NSUTF8StringEncoding];
        [returnDictionary setObject:password forKey:(__bridge id)kSecValueData];
    }
    // Don't do anything if nothing is found.
    else if (keychainError == errSecItemNotFound) {
            NSAssert(NO, @"Nothing was found in the keychain.\n");
            if (passwordData) CFRelease(passwordData);
    }
    // Any other error is unexpected.
    else
    {
        NSAssert(NO, @"Serious error.\n");
        if (passwordData) CFRelease(passwordData);
    }
 
    return returnDictionary;
}
 
// Implement the writeToKeychain method, which is called by the mySetObject routine,
//   which in turn is called by the UI when there is new data for the keychain. This
//   method modifies an existing keychain item, or--if the item does not already
//   exist--creates a new keychain item with the new attribute value plus
//  default values for the other attributes.
- (void)writeToKeychain
{
    CFDictionaryRef attributes = nil;
    NSMutableDictionary *updateItem = nil;
 
    // If the keychain item already exists, modify it:
    if (SecItemCopyMatching((__bridge CFDictionaryRef)genericPasswordQuery,
                           (CFTypeRef *)&attributes) == noErr)
    {
        // First, get the attributes returned from the keychain and add them to the
        // dictionary that controls the update:
        updateItem = [NSMutableDictionary dictionaryWithDictionary:(__bridge_transfer NSDictionary *)attributes];
 
        // Second, get the class value from the generic password query dictionary and
        // add it to the updateItem dictionary:
        [updateItem setObject:[genericPasswordQuery objectForKey:(__bridge id)kSecClass]
                                                          forKey:(__bridge id)kSecClass];
 
        // Finally, set up the dictionary that contains new values for the attributes:
        NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainData];
        //Remove the class--it's not a keychain attribute:
        [tempCheck removeObjectForKey:(__bridge id)kSecClass];
 
        // You can update only a single keychain item at a time.
        OSStatus errorcode = SecItemUpdate(
            (__bridge CFDictionaryRef)updateItem,
            (__bridge CFDictionaryRef)tempCheck);
        NSAssert(errorcode == noErr, @"Couldn't update the Keychain Item." );
    }
    else
    {
        // No previous item found; add the new item.
        // The new value was added to the keychainData dictionary in the mySetObject routine,
        // and the other values were added to the keychainData dictionary previously.
        // No pointer to the newly-added items is needed, so pass NULL for the second parameter:
        OSStatus errorcode = SecItemAdd(
            (__bridge CFDictionaryRef)[self dictionaryToSecItemFormat:keychainData],
            NULL);
        NSAssert(errorcode == noErr, @"Couldn't add the Keychain Item." );
        if (attributes) CFRelease(attributes);
    }
}
 
 
@end

Этот пример следует за той же общей последовательностью, показанной на рисунке 2-1. В этой выборке универсальный атрибут используется для создания уникальной строки, которая может использоваться для простой идентификации элемента цепочки для ключей. При необходимости можно использовать стандартные атрибуты, такие как имя службы и имя пользователя в той же цели.

Для получения дополнительной информации

Для дополнительного примера Цепочки для ключей iPhone см. проект примера кода GenericKeychain.