keychain service钥匙串服务

来源:互联网 发布:阿里云过户域名流程 编辑:程序博客网 时间:2024/04/30 09:42

keychain service钥匙串服务是iOS提供的用于管理用户密码、密钥、证书、标识的服务,它相当于一个加密容器,app可以把相关的用户信息以钥匙条目的形式存储到其中,钥匙串服务会将所有存储到其中的条目进行加密,并保护起来,只允许创建这个条目的app访问它。

app要把用户信息(钥匙)添加到钥匙串服务(钥匙串)中,在必要时从钥匙串服务中取出用户信息,这些动作需要用到Keychain Services API。Keychain Services API的相关属性和方法声明在< Security/security.h >中。

一、操作钥匙条目的三个方法:

1、添加钥匙条目到钥匙串
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef * __nullable CF_RETURNS_RETAINED result)
2、修改钥匙条目
OSStatus SecItemUpdate(CFDictionaryRef query,CFDictionaryRef attributesToUpdate)
3、提取钥匙信息
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef * __nullable CF_RETURNS_RETAINED result)

二、添加钥匙条目到钥匙串

添加钥匙条目到钥匙串可以调用SecItemAdd方法,但前提是要先要将用户信息封装成钥匙条目secItem,才可以加入到钥匙串。钥匙条目实际上就是一个钥匙属性字典,将需要存储的用户信息加到这个字典中,再插入一些与钥匙属性相关的键值对,就形成了一个钥匙条目,然后就可以加入到钥匙串里了。这个字典的特殊在于,它有许多预定好的key,用户信息,以及钥匙条目的属性信息,加到该字典(调用setValue:forKey:)指定的key中。
下面来了解钥匙条目字典中都有哪些key。

1、与用户信息相关的key

kSecValueData ,这个key对应CFDataRef类型的value,把要存储的用户信息放到这里,这里的内容如果是秘钥或密码,钥匙串服务就会对它们进行加密存储。

2、与钥匙条目的属性相关的key

根据存储的用户信息的不同,钥匙串服务把钥匙条目分成了几类:普通密码(kSecClassGenericPassword)条目、互联网密码(kSecClassInternetPassword)条目、密钥(kSecClassKey)条目、证书(kSecClassCertificate)条目和标识(kSecClassIdentity)条目。不同类型的钥匙条目有各自的属性key。

(1)普通密码钥匙条目的key有:

    kSecAttrAccessible    kSecAttrAccessControl    kSecAttrAccessGroup    kSecAttrCreationDate    kSecAttrModificationDate    kSecAttrDescription    kSecAttrComment    kSecAttrCreator    kSecAttrType    kSecAttrLabel    kSecAttrIsInvisible    kSecAttrIsNegative    kSecAttrAccount    kSecAttrService    kSecAttrGeneric    kSecAttrSynchronizable

(2)网络密码钥匙条目的key有:

    kSecAttrAccessible    kSecAttrAccessControl    kSecAttrAccessGroup    kSecAttrCreationDate    kSecAttrModificationDate    kSecAttrDescription    kSecAttrComment    kSecAttrCreator    kSecAttrType    kSecAttrLabel    kSecAttrIsInvisible    kSecAttrIsNegative    kSecAttrAccount    kSecAttrSecurityDomain    kSecAttrServer    kSecAttrProtocol    kSecAttrAuthenticationType    kSecAttrPort    kSecAttrPath    kSecAttrSynchronizable

(3)证书钥匙条目的key有:

    kSecAttrAccessible    kSecAttrAccessControl    kSecAttrAccessGroup    kSecAttrCertificateType    kSecAttrCertificateEncoding    kSecAttrLabel    kSecAttrSubject    kSecAttrIssuer    kSecAttrSerialNumber    kSecAttrSubjectKeyID    kSecAttrPublicKeyHash    kSecAttrSynchronizable

(4)秘钥条目的key有:

    kSecAttrAccessible    kSecAttrAccessControl    kSecAttrAccessGroup    kSecAttrKeyClass    kSecAttrLabel    kSecAttrApplicationLabel    kSecAttrIsPermanent    kSecAttrApplicationTag    kSecAttrKeyType    kSecAttrKeySizeInBits    kSecAttrEffectiveKeySize    kSecAttrCanEncrypt    kSecAttrCanDecrypt    kSecAttrCanDerive    kSecAttrCanSign    kSecAttrCanVerify    kSecAttrCanWrap    kSecAttrCanUnwrap    kSecAttrSynchronizable

(5)标识钥匙条目的key有:

钥匙串服务中所说的标识identity就是证书(公钥)和秘钥(私钥)的结合。因此标识钥匙条目的属性字典中的key是证书钥匙条目的key加上密钥钥匙条目的key。

(6)部分key的意义,及其对应value的类型

这里写图片描述

三、从钥匙串中查询钥匙条目

要从钥匙串中查询钥匙条目,需要调用这个方法SecItemCopyMatching方法,查询的本质其实是匹配,也就说需要先提供有关钥匙条目的相关信息,拿着这些信息到钥匙串中进行匹配,如果能找到有匹配的,就相当于查询到了钥匙条目,然后将它取出。

1、提供有关钥匙条目的相关信息

有关要查询的钥匙条目的信息,是以一个字典的形式提供给SecItemCopyMatching方法的(第一个参数)。因此要查询钥匙条目,第一件事是新建一个字典(姑且称作查询字典),并插入有关钥匙条目的信息。该字典也是有许多预定义的key的,有些key对应的value是必须要设置的。

查询字典的预定义key有许多,一个查询字典通常包括以下key:
这里写图片描述

(1)若干钥匙条目的属性key

也就是上面说到的,创建一个钥匙条目时可以用的各种key。

(2)若干与匹配条件相关的key

如果给这种key设置了value就相当于多了一条匹配条件,钥匙条目字典的某个键值对要符合这个条件才匹配。不设置则不生效。
这里写图片描述

(3)若干与查询结果类型相关的key

这里写图片描述

2、获取查询结果

提供了查询字典,接下来就是获取查询结果,需要有一个CFTypeRef类型变量来承接查询结果(传入SecItemCopyMatching方法的第二个参数)。
在结果字典中,可以通过指定key来获取到有关钥匙条目的信息
这里写图片描述

三、实际应用

使用钥匙串服务的一般思路是,在app请求登录的时候,先到钥匙串中找相应的钥匙,如果找到,则登录,如果找不到,则提示用户输入登录信息,如果登录成功,则询问用户是否将保存登录信息,如果同意,则新建钥匙添加到钥匙串。

要在app中使用钥匙串服务,最好就是把钥匙条目的新建、查询、更新、删除等逻辑封装成类,如何实现呢?
官网上有一个利用Keychain Services API的例子,把钥匙条目的新建、查询、更新、删除等逻辑封装成了一个类KeychainWrapper。其实我们可以直接使用这个类就不用自己写了,但是还是有必要了解一下具体实现思路,才能够在需要时修改这个类以适应自己的app。

这个类管理定义了一个字典属性keychainData来存储钥匙条目的信息,提供了类的初始化方法,以及三个操作该钥匙条目的方法:

1、该类的初始化方法
-(instanceType)init;
该方法的实现思路是,新建一个查询字典genericPasswordQuery,配置相关匹配条件,将然后到钥匙串中查找是否有匹配结果,若有则将匹配到的钥匙条目信息存储到keychainData中;若没有就调用重置钥匙条目的方法(下面说),初始化keychainData中的钥匙条目信息。
2、往这个钥匙条目字典中插入键值对
- (void)mySetObject:(id)inObject forKey:(id)key;
这个方法的实现思路是,将新的键值对插入keychainData中。先查找钥匙串中是否存在匹配genericPasswordQuery的钥匙条目,如果存在,则取出,插入新的键值对,并更新钥匙条目;如果不存在,则用keychainData创建钥匙条目。
3、从重置钥匙条目
- (void)resetKeychainItem;
这个方法的实现思路是,先判断keychainData是否为空,如果为空就初始化keychainData,并插入默认的键值对;如果不为空,就去钥匙串里查找匹配keychainData的钥匙条目,删除该钥匙条目,再次向keychainData插入默认的键值对。
4、提取钥匙条目中的信息
- (id)myObjectForKey:(id)key;
这个方法的实现是直接通过key访问keychainData中的钥匙条目信息。

最后附上这个类:

KeychainWrapper.h

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

KeychainWrapper.m

#import "KeychainWrapper.h"@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:@"ssssssss" 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
1 0