iOS Keychain使用
iOS的keychain可以说是系统里唯一可以做到安全可靠存储应用敏感数据并且可以在应用卸载或重新安装时仍然保留其数据的地方。当使用itunes进行数据备份时,每个应用程序在keychain里的数据都会得到备份,而且备份的数据是经过加密的,所以无论数据存储在哪里,都十分安全。鉴于keychain的这些特性,使得它成为开发者存储应用敏感数据的首选,应用程序常见的敏感数据通常有密码,秘钥等等。
然而,开发者面临的一个问题是,iOS sdk里提供的keychain接口十分隐晦难用,初学者在理解及应用上遇到了不小的麻烦,甚至Apple官方提供的keychain示例代码GenericKeychain也都十分难读懂。因此,本文试图将keychain的使用进行一下梳理,让初学者能够了解其基本使用。Keychain API的用法稍微有点复杂。
不过Apple自己也提供了一个封装了Keychain API的类: KeychainItemWrapperhttps://developer.apple.com/library/ios/samplecode/GenericKeychain/Introduction/Intro.html虽然这个类封装了Keychain的API,但是不仅代码写的很不容易理解,而且里面也有不少的Bug。所以还是不建议使用。 目前发现这个类的1.2版存在的Bug为:
如果需要某个keychain item支持iCloud备份,添加kSecAttrSynchronizable属性之后,它并没有在第二次更新item或者搜索item的时候加上这一条,所以导致item已经存在但是它却获取不到。
类型为GenericPassword的item必须使用kSecAttrAccount和kSecAttrService作为主要的key,但是这个类仅仅以kSecAttrGeneric作主要的key。所以在用它添加item的时候容易出现重复添加的错误。
每种类型的Keychain item都有不同的键作为主要的Key也就是唯一标示符用于搜索,更新和删除,Keychain内部不允许添加重复的Item。
准备工作
准备使用keychain接口前,首先需要在xcode里加入Security.framework,然后在代码里加入头文件 "#import "。注意,如果你使用的是object-c,Security.framework里的接口都是C语言风格接口而不是object-c语言风格的接口。此外,Security.framework只工作在实际设备上,无法在模拟器上使用。
Keychain概览
Keychain里可以存储若干条目(item),每个条目都属于某一个类别(class),以下是常见的几种类别:
kSecClassInternetPassword
属于该类别的条目往往用来存储上网登录密码,远程服务器密码等
kSecClassGenericPassword
存储一些通用的密码,比如数据库密码,vpn连接的密码等等
kSecClassCertificate, kSecClassKey和kSecClassIdentity
这三类条目往往用于建立基于证书,秘钥和公钥系统的安全连接。
条目类别是一个条目最基本的属性,每个存入keychain的条目,都需要为它制定一个类别。
操作keychain常见的3个方法:
SecItemAdd
添加新条目到keychain.
SecItemUpdate
修改一个已存在keychain里的条目
SecItemCopyMatching
查询keychain里的条目并且读取条目信息
下面用一张摘取自苹果官方网站的一张图阐述一下一个常见keychain的操作流程,该图以一个应用连接ftp服务器为例
在上图中,应用大概流程是这样的:应用开始连接ftp服务器,首先查询keychain是否存在服务器密码,如果存在,那么直接取出密码登陆;如果不存在,那么应用弹出对话框要求用户输入,然后进行登陆,如果登陆成功,那么存储密码到keychain。
简单来说,如果要存储一个password,需要先遍历Keychain,看它的password是否已经存在于Keychain中,存在的话就更新它的值,不存在就存储。
所以这就使用到苹果提供的方法:
// 查询OSStatus SecItemCopyMatching(CFDictionaryRefquery,CFTypeRef*result);// 添加OSStatus SecItemAdd(CFDictionaryRefattributes,CFTypeRef*result);// 更新OSStatus SecItemUpdate(CFDictionaryRefquery,CFDictionaryRefattributesToUpdate);// 删除OSStatus SecItemDelete(CFDictionaryRefquery)
接下来看看操作Keychain常用的key-value.
kSecClass:有五个值,分别为
kSecClassGenericPassword(通用密码--也是接下来使用的)、
kSecClassInternetPassword(互联网密码)
kSecClassCertificate(证书)、
kSecClassKey(密钥)、
kSecClassIdentity(身份)
kSecAttrService:服务
kSecAttrServer:服务器域名或IP地址
kSecAttrAccount:账号
kSecAttrAccessGroup: 可以在应用之间共享keychain中的数据
kSecMatchLimit:返回搜索结果,
kSecMatchLimitOne(一个)、
kSecMatchLimitAll(全部)
Keychain接口参数
所有对keychain接口的操作,参数的传递基本上都用到字典,将所需要的参数放入字典,然后将字典传递给keychain接口。上面提到的条目类别,就是一个参数,除此之外,操作条目往往还需要条目ID,条目所属服务名,条目所属账户名等。这些参数的名称如下:
kSecClass:条目类别
kSecAttrGeneric:条目id
kSecAttrService:条目所属服务
kSecAttrAccount:条目所属账户名
其中kSecAttrService和kSecAttrAccount在整个keychain里必须唯一,不能重名。
Keychain条目查询
使用SecItemCopyMatching进行查询,查询时,我们需要指明要查询条目的类别(kSecClass),条目的id(kSecAttrGeneric),条目所属的服务和账户(kSecAttrService,kSecAttrAccount)。此外,我们还可以设置其他一些查询条件,比如返回条目的数量(kSecMatchLimit),返回条目的数据类型,比如:
kSecReturnData:返回条目所存储的数据,返回值类型是CFDataRef
kSecReturnAttributes:返回该条目的属性,返回值是字典类型CFDictionaryRef
kSecReturnRef:返回条目的引用,根据条目所属类别,返回值类型可能是:SecKeychainItemRef, SecKeyRef,SecCertificateRef, SecIdentityRef.
kSecReturnPersistentRef:返回条目的引用,返回值类型是CFDataRef
如下代码示例用来查询存储在keychain里的密码
//建立词典,用来传递参数
NSMutableDictionary *dictionary = [[NSMutableDictionary dictionary];
//设置条目类别,我们用该条目存储普通密码,所以设置成kSecClassGenericPassword
[dictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
//设置条目的id,比如“MyPasswordForFtp",条目id必须时NSDate,而不是NSString
NSString *itemIDString = @"MyPasswordForFtp";
NSData *itemID= [itemIDString dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:itemIDforKey:(id)kSecAttrGeneric];
//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.ftppassword
NSString *account = @"com.mykeychain.ftppassword";
NSString *service = @"com.mykeychain.ftppassword";
[dictionary setObject:account forKey:(id)kSecAttrAccount];
[dictionary setObject:service forKey:(id)kSecAttrService];
//设置查询条件,只返回一个条目
[dictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
//设置查询条件,返回条目存储的数据 (kSecReturnData == True)
[dictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
//开始查询
NSData *result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)dictionary , (CFTypeRef *)&result);
Keychain条目添加
使用SecItemAdd()进行条目添加,我们需要指明新条目的类别(kSecClass),新条目的id(kSecAttrGeneric),新条目所属的服务和账户(kSecAttrService,kSecAttrAccount),此外,还需要指明该条目所存储的数据(kSecValueData),否则,没有数据的条目就没有任何存入keychain的意义了。
请看如下代码示例:
//建立词典,用来传递参数
NSMutableDictionary *dictionary = [[NSMutableDictionary dictionary];
//设置条目类别,我们用该条目存储普通密码,所以设置成kSecClassGenericPassword
[dictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
//设置条目的id,比如“MyPasswordForFtp",条目id必须时NSDate,而不是NSString
NSString *itemIDString = @"MyPasswordForFtp";
NSData *itemID= [itemIDString dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:itemIDforKey:(id)kSecAttrGeneric];
//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.ftppassword
NSString *account = @"com.mykeychain.ftppassword";
NSString *service = @"com.mykeychain.ftppassword";
[dictionary setObject:account forKey:(id)kSecAttrAccount];
[dictionary setObject:service forKey:(id)kSecAttrService];
//设置条目数据,条目数据时NSDate
NSString *password = @"123456";
NSData *itemData = [password dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:itemData forKey:(id)kSecValueData];
//添加条目到keychain
OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL);
keychain条目的修改
使用SecItemUpdate()进行条目修改,需要传入两个词典,第一个词典用来查询条目,第二个词典用来传递修改后的新值。查询条目所用的词典设置请看上述Keychain条目查询章节,更新值所用词典里设置要更新的参数和值。这里请注意的是,查询词典里不能设置任何查询条件,比如kSecMatchLimit和kSecReturnData,kSecReturnAttributes等,否则会出错。因为这里的查询是为了更新数据,是以写入为目的,不是真正的查询和读取条目,所以设置这些和读取有关的条件是无意义的,系统会认为是和安全有关的错误,会返回-50(errSecParam)
下面代码用来更新密码,要更新的条目id为“MyPasswordForFtp”的条目
//建立查询字典
NSMutableDictionary *searchDictionary = [[NSMutableDictionary dictionary];
//设置查询字典
[searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
NSString *itemIDString = @"MyPasswordForFtp";
NSData *itemID= [itemIDString dataUsingEncoding:NSUTF8StringEncoding];
[searchDictionary setObject:itemIDforKey:(id)kSecAttrGeneric];
NSString *account = @"com.mykeychain.ftppassword";
NSString *service = @"com.mykeychain.ftppassword";
[searchDictionary setObject:account forKey:(id)kSecAttrAccount];
[searchDictionary setObject:service forKey:(id)kSecAttrService];
// 建立更新字典
NSMutableDictionary *updateDictionary = [[NSMutableDictionary dictionary];
// 设置要更新的新密码
NSString* newPassword = @"654321";
NSData *passwordData = [newPassword dataUsingEncoding:NSUTF8StringEncoding];
[updateDictionary setObject:passwordData forKey:(id)kSecValueData];
//进行更新
OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary, (CFDictionaryRef)updateDictionary);
keychain接口常见错误
SecItemUpdate 返回-50(errSecParam):请检查查询词典里是否存在读取条件,比如kSecReturnData,kSecMatchLimit等
SecItemAdd 返回-25299 (errSecDuplicateItem: 请检查kSecAttrAccount和kSecAttrService是否已经存在于keychain中,请尝试设置其他值避免重复
官方文档:https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/iPhoneTasks/iPhoneTasks.html#//apple_ref/doc/uid/TP30000897-CH208-SW1
https://zhuanlan.zhihu.com/p/22731783
http://www.360doc.com/content/15/0708/15/20918780_483580050.shtml
http://blog.51cto.com/13619586/2074256
https://www.jianshu.com/p/e92e4e4b41a5