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