원문 : https://developer.apple.com/library/mac/documentation/Security/Conceptual/keychainServConcepts/iPhoneTasks/iPhoneTasks.html


iOS 키체인 서비스 작업 


이번 장에서는 iOS와 OS X에서 모두 사용 할수 있는 기본 키체인 서비스 기능을 사용하는 방법을 보여준다.

이번 장에서 설명하는 기능은 다음과 같은 이점을 제공한다. 

  • 키체인에 아이템을 추가 
  • 키체인의 아이템을 검색
  • 키체인 아이템의 속성과 데이터를 가져오기
  • 키체인 아이템의 속성과 데이터를 변경하기
키체인 서비스 개념(Keychain Services Concepts)은 키체인 서비스의 개념과 용어에 대한 소개를 제공한다. 모든 키체인 서비스 함수에 대한 자세한 내용은 키체인 서비스 레퍼런스(Keychain Services Reference)를 보라 


메모 : 아이폰에서, 키체인 권한은 여러분의 응용프로그램 서명에 사용된 프로비져닝 프로파일에 의존한다. 여러분 응용 프로그램의 다른 버젼에서도 동일한 프로비저닝 프로파일을 지속적으로 사용해야 한다.


여러분의 응용프로그램에 키체인 서비스를 추가하기 


대부분의 iOS 응용프로그램은 키체인에 새로운 함호를 추가하고, 기존의 키체인 아이템을 수정, 필요할때 암호를 검색하기 위해  키체인 서비스 사용을 필요로 한다. 키체인 서비스는 이러한 작업들을 수행하기 위해 다음의 함수들을 제공한다.

  • SecItemAdd : 키체인에 아이템을 추가 
  • SecItemUpdate : 기존의 키체인 아이템을 수정
  • SecItemCopyMatching : 키체인 아이템을 찾거나 그 정보를 추출
여러분이 인터넷에서 서버와 웹 사이트들을 접속하기 위해 인터넷 패스워드를 사용한다. 그리고 다른 암호로 보호된 서비스에 대해 일반적으로 암호(예를 들면 데이터베이스와 스케쥴링 응용프로그램). iOS 키체인 서비스에서 인증서, 키, 아이디가 저장되고, 서로 다른 특성을 갖는 것을 제외하면 암호와 동일한 방식으로 검색한다.

다음 그림은 응용프로그램이 인터넷 FTP서버에 접근하기 위해 사용할수 있는 방법의 흐름도를 보여준다.

아이폰 키체인 서비스를 사용하여 인터넷 서버에 접근


응용프로그램 사용자가 파일전송 프로토콜(FTP) 서버를 선택함으로써 시작한다. 이 응용프로그램은 키 체인 항목을 식별하는 속성을 포함하는 딕셔너리를 전달하는 SecItemCopyMatching 호출한다. 만약 키체인에 암호가 있다면, 그 함수는 응용프로그램에게 암호를 반환한다. 사용자 인증을 FTP 서버에 보낸다. 만약 인증이 성공한다면, 이 처리작업은 종료된다. 만약 인증이 실패하면, 응용프로그램은 사용자 이름과 암호를 요구하는 다이얼로그를 보여준다. 


만약 키체인에 암호가 없는 경우에, SecItemCopyMatching은 errSecItemNotFound 결과 코드를 반환한다. 이 경우에도 응용프로그램은 사용자 이름과 암호를 요구하는 다이얼 로그를 보여준다. (이 다이얼로그는 취소버튼을 포함한다, 하지만 이런 선택은 지나치게 복잡해 지므로 흐름도에서 생략한다)


사용자의 암호를 얻은 후, 응용프로그램은 FTP 서버에 사용자의 인증을 진행한다. 인증이 성공하면, 응용프로그램은 사용자가 입력한 정보가 유효하다고 가정할수 있다. 응용프로그램이 키 체인 암호를 저장할지 여부를 묻는 다른 대화상자가 표시된다. 만약 사용자가 No를 선택하면 이 작업은 종료된다. 만약 사용자가 Yes를 선택하면 응용프로그램은 SecItemAdd함수(만약 이것이 새로인 키체인 아이템이라면) 또는 SecitemUpdate함수(기존에 존재하는 키체인 아이템을 업데이트하는 경우) 를 호출하고 난 뒤에 작업을 마친다.


다음은 어떻게 응용프로그램이 키체인 서비스 함수를 사용하여 일반 항목에 대한 암호를 가져오고 설정하는지 보여준다.  여러분은 이와 같은 기능을 사용하여 키체인 아이템 속성(예를 들면 사용자 이름이나 서비스 이름)을 가져오거나 설정할수 있다.

#import <Foundation/Foundation.h>
#import <Security/Securtity.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


이 예제는 위의 순서도 예제에 표시된것과 같은 순서를 보여준다.  이 샘플에서 일반적인 속성은 키체인 아이템을 쉽게 식별할수 있는 고유한 문자열을 생성하는데 사용된다. 필요하다면, 여러분은 같은 목적을 위해 서비스 이름과 사용자 이름으로 표준 속성을 사용할수 있다.


더 많은 정보


추가적으로 아이폰 키체인 예제는 GenericKeychain 샘플코드 프로젝트를 보라.



저작자 표시 비영리 변경 금지
신고
Posted by 까칠코더.


티스토리 툴바