iOS的keychain可以说是系统里唯一可以做到安全可靠存储应用敏感数据并且可以在应用卸载或重新安装时仍然保留其数据的地方。当使用itunes进行数据备份时,每个应用程序在keychain里的数据都会得到备份,而且备份的数据是经过加密的,所以无论数据存储在哪里,都十分安全。鉴于keychain的这些特性,使得它成为开发者存储应用敏感数据的首选,应用程序常见的敏感数据通常有密码,秘钥等等。

然而,开发者面临的一个问题是,iOS sdk里提供的keychain接口十分隐晦难用,初学者在理解及应用上遇到了不小的麻烦,甚至Apple官方提供的keychain示例代码GenericKeychain也都十分难读懂。因此,本文试图将keychain的使用进行一下梳理,让初学者能够了解其基本使用。


准备工作

准备使用keychain接口前,首先需要在xcode里加入Security.framework,然后在代码里加入头文件 "#import <Security/Security.h>"。注意,如果你使用的是object-c,Security.framework里的接口都是C语言风格接口而不是object-c语言风格的接口。此外,Security.framework只工作在实际设备上,无法在模拟器上使用。


Keychain概览

Keychain里可以存储若干条目(item),每个条目都属于某一个类别(class),以下是常见的几种类别:

kSecClassInternetPassword

属于该类别的条目往往用来存储上网登录密码,远程服务器密码等

kSecClassGenericPassword

存储一些通用的密码,比如数据库密码,***连接的密码等等

kSecClassCertificate,kSecClassKey和kSecClassIdentity

这三类条目往往用于建立基于证书,秘钥和公钥系统的安全连接。


条目类别是一个条目最基本的属性,每个存入keychain的条目,都需要为它制定一个类别。

操作keychain常见的3个方法:

SecItemAdd

添加新条目到keychain.

SecItemUpdate

修改一个已存在keychain里的条目

SecItemCopyMatching

查询keychain里的条目并且读取条目信息


下面用一张摘取自苹果官方网站的一张图阐述一下一个常见keychain的操作流程,该图以一个应用连接ftp服务器为例


在上图中,应用大概流程是这样的:应用开始连接ftp服务器,首先查询keychain是否存在服务器密码,如果存在,那么直接取出密码登陆;如果不存在,那么应用弹出对话框要求用户输入,然后进行登陆,如果登陆成功,那么存储密码到keychain。


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=[[NSMutableDictionarydictionary];//设置条目类别,我们用该条目存储普通密码,所以设置成kSecClassGenericPassword[dictionarysetObject:(id)kSecClassGenericPasswordforKey:(id)kSecClass];//设置条目的id,比如“MyPasswordForFtp",条目id必须时NSDate,而不是NSStringNSString*itemIDString=@"MyPasswordForFtp";NSData*itemID=[itemIDStringdataUsingEncoding:NSUTF8StringEncoding];[dictionarysetObject:itemIDforKey:(id)kSecAttrGeneric];//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.ftppasswordNSString*account=@"com.mykeychain.ftppassword";NSString*service=@"com.mykeychain.ftppassword";[dictionarysetObject:accountforKey:(id)kSecAttrAccount];[dictionarysetObject:serviceforKey:(id)kSecAttrService];//设置查询条件,只返回一个条目[dictionarysetObject:(id)kSecMatchLimitOneforKey:(id)kSecMatchLimit];//设置查询条件,返回条目存储的数据(kSecReturnData==True)[dictionarysetObject:(id)kCFBooleanTrueforKey:(id)kSecReturnData];//开始查询NSData*result=nil;OSStatusstatus=SecItemCopyMatching((CFDictionaryRef)dictionary,(CFTypeRef*)&result);


Keychain条目添加

使用SecItemAdd()进行条目添加,我们需要指明新条目的类别(kSecClass),新条目的id(kSecAttrGeneric),新条目所属的服务和账户(kSecAttrService,kSecAttrAccount),此外,还需要指明该条目所存储的数据(kSecValueData),否则,没有数据的条目就没有任何存入keychain的意义了。

请看如下代码示例:

//建立词典,用来传递参数NSMutableDictionary*dictionary=[[NSMutableDictionarydictionary];//设置条目类别,我们用该条目存储普通密码,所以设置成kSecClassGenericPassword[dictionarysetObject:(id)kSecClassGenericPasswordforKey:(id)kSecClass];//设置条目的id,比如“MyPasswordForFtp",条目id必须时NSDate,而不是NSStringNSString*itemIDString=@"MyPasswordForFtp";NSData*itemID=[itemIDStringdataUsingEncoding:NSUTF8StringEncoding];[dictionarysetObject:itemIDforKey:(id)kSecAttrGeneric];//设置条目所属的服务和账户,为了避免重名,我们使用常见的反转域名规则,比如com.mykeychain.ftppasswordNSString*account=@"com.mykeychain.ftppassword";NSString*service=@"com.mykeychain.ftppassword";[dictionarysetObject:accountforKey:(id)kSecAttrAccount];[dictionarysetObject:serviceforKey:(id)kSecAttrService];//设置条目数据,条目数据时NSDateNSString*password=@"123456";NSData*itemData=[passworddataUsingEncoding:NSUTF8StringEncoding];[dictionarysetObject:itemDataforKey:(id)kSecValueData];//添加条目到keychainOSStatusstatus=SecItemAdd((CFDictionaryRef)dictionary,NULL);


keychain条目的修改

使用SecItemUpdate()进行条目修改,需要传入两个词典,第一个词典用来查询条目,第二个词典用来传递修改后的新值。查询条目所用的词典设置请看上述Keychain条目查询章节,更新值所用词典里设置要更新的参数和值。这里请注意的是,查询词典里不能设置任何查询条件,比如kSecMatchLimit和kSecReturnData,kSecReturnAttributes等,否则会出错。因为这里的查询是为了更新数据,是以写入为目的,不是真正的查询和读取条目,所以设置这些和读取有关的条件是无意义的,系统会认为是和安全有关的错误,会返回-50(errSecParam)


下面代码用来更新密码,要更新的条目id为“MyPasswordForFtp”的条目

//建立查询字典NSMutableDictionary*searchDictionary=[[NSMutableDictionarydictionary];//设置查询字典[searchDictionarysetObject:(id)kSecClassGenericPasswordforKey:(id)kSecClass];NSString*itemIDString=@"MyPasswordForFtp";NSData*itemID=[itemIDStringdataUsingEncoding:NSUTF8StringEncoding];[searchDictionarysetObject:itemIDforKey:(id)kSecAttrGeneric];NSString*account=@"com.mykeychain.ftppassword";NSString*service=@"com.mykeychain.ftppassword";[searchDictionarysetObject:accountforKey:(id)kSecAttrAccount];[searchDictionarysetObject:serviceforKey:(id)kSecAttrService];//建立更新字典NSMutableDictionary*updateDictionary=[[NSMutableDictionarydictionary];//设置要更新的新密码NSString*newPassword=@"654321";NSData*passwordData=[newPassworddataUsingEncoding:NSUTF8StringEncoding];[updateDictionarysetObject:passwordDataforKey:(id)kSecValueData];//进行更新OSStatusstatus=SecItemUpdate((CFDictionaryRef)searchDictionary,(CFDictionaryRef)updateDictionary);

keychain接口常见错误

SecItemUpdate 返回-50(errSecParam):请检查查询词典里是否存在读取条件,比如kSecReturnData,kSecMatchLimit等

SecItemAdd 返回-25299 (errSecDuplicateItem: 请检查kSecAttrAccount和kSecAttrService是否已经存在于keychain中,请尝试设置其他值避免重复