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。
kSecValueData ,这个key对应CFDataRef类型的value,把要存储的用户信息放到这里,这里的内容如果是秘钥或密码,钥匙串服务就会对它们进行加密存储。
根据存储的用户信息的不同,钥匙串服务把钥匙条目分成了几类:普通密码(kSecClassGenericPassword)条目、互联网密码(kSecClassInternetPassword)条目、密钥(kSecClassKey)条目、证书(kSecClassCertificate)条目和标识(kSecClassIdentity)条目。不同类型的钥匙条目有各自的属性key。
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrService
kSecAttrGeneric
kSecAttrSynchronizable
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrCreationDate
kSecAttrModificationDate
kSecAttrDescription
kSecAttrComment
kSecAttrCreator
kSecAttrType
kSecAttrLabel
kSecAttrIsInvisible
kSecAttrIsNegative
kSecAttrAccount
kSecAttrSecurityDomain
kSecAttrServer
kSecAttrProtocol
kSecAttrAuthenticationType
kSecAttrPort
kSecAttrPath
kSecAttrSynchronizable
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrCertificateType
kSecAttrCertificateEncoding
kSecAttrLabel
kSecAttrSubject
kSecAttrIssuer
kSecAttrSerialNumber
kSecAttrSubjectKeyID
kSecAttrPublicKeyHash
kSecAttrSynchronizable
kSecAttrAccessible
kSecAttrAccessControl
kSecAttrAccessGroup
kSecAttrKeyClass
kSecAttrLabel
kSecAttrApplicationLabel
kSecAttrIsPermanent
kSecAttrApplicationTag
kSecAttrKeyType
kSecAttrKeySizeInBits
kSecAttrEffectiveKeySize
kSecAttrCanEncrypt
kSecAttrCanDecrypt
kSecAttrCanDerive
kSecAttrCanSign
kSecAttrCanVerify
kSecAttrCanWrap
kSecAttrCanUnwrap
kSecAttrSynchronizable
钥匙串服务中所说的标识identity就是证书(公钥)和秘钥(私钥)的结合。因此标识钥匙条目的属性字典中的key是证书钥匙条目的key加上密钥钥匙条目的key。
要从钥匙串中查询钥匙条目,需要调用这个方法SecItemCopyMatching
方法,查询的本质其实是匹配,也就说需要先提供有关钥匙条目的相关信息,拿着这些信息到钥匙串中进行匹配,如果能找到有匹配的,就相当于查询到了钥匙条目,然后将它取出。
有关要查询的钥匙条目的信息,是以一个字典的形式提供给SecItemCopyMatching
方法的(第一个参数)。因此要查询钥匙条目,第一件事是新建一个字典(姑且称作查询字典),并插入有关钥匙条目的信息。该字典也是有许多预定义的key的,有些key对应的value是必须要设置的。
查询字典的预定义key有许多,一个查询字典通常包括以下key:
也就是上面说到的,创建一个钥匙条目时可以用的各种key。
如果给这种key设置了value就相当于多了一条匹配条件,钥匙条目字典的某个键值对要符合这个条件才匹配。不设置则不生效。
提供了查询字典,接下来就是获取查询结果,需要有一个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