作为iOS开发者,我们所熟知的本地化存储方案是plist、NSUserDefaults、NSKeyedArchiver、数据库(FMDB/CoreData)。然而这几种存储方式终究逃不开沙盒系统,如果App被卸载,储存的数据也不复存在。要想规避这种情况或者想多个App共用数据,我们就要把数据存储在keychain(钥匙串)中。下面我们就聊聊Keychain那些事。
什么是Keychain
根据苹果的介绍,iOS设备中的Keychain是一个安全的存储容器,可以用来为不同应用保存敏感信息比如用户名,密码,网络密码,认证令牌。苹果自己用keychain来保存Wi-Fi网络密码,VPN凭证等等。它是一个在所有app之外的sqlite数据库。
Keychain内部可以保存很多的信息。每条信息作为一个单独的keychain item。keychain item一般为一个字典,每条keychain item包含一条data和很多attributes。举个例子,一个用户账户就是一条item,用户名可以作为一个attribute , 密码就是data。
如何使用Keychain保存数据
使用keychain一般有三种方法
第一种 原生API(缺点:操作复杂)
SecItemAdd 添加一个keychain item
SecItemUpdate 修改一个keychain item
SecItemCopyMatching 搜索一个keychain item
SecItemDelete 删除一个keychain item
通过如下代码了解下原生API的使用
NSMutableDictionary *searchDictionary = [[NSMutableDictionary alloc] init];
//指定item的类型为GenericPassword
[searchDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
//类型为GenericPassword的信息必须提供以下两条属性作为unique identifier
[searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrAccount];
[searchDictionary setObject:encodedIdentifier forKey:(id)kSecAttrService];
return searchDictionary;
}
- (NSData *)searchKeychainCopyMatching:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
//在搜索keychain item的时候必须提供下面的两条用于搜索的属性
//只返回搜索到的第一条item,这个是搜索条件。
[searchDictionary setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
//返回item的kSecValueData 字段。也就是我们一般用于存放的密码,返回类型为NSData *类型
[searchDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
//我来解释下这里匹配出的是 找到一条符合ksecAttrAccount、类型为普通密码类型kSecClass,返回ksecValueData字段。
NSData *result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchDictionary,
(CFTypeRef *)&result);
[searchDictionary release];
return result;
}
- (BOOL)createKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier {
NSMutableDictionary *dictionary = [self newSearchDictionary:identifier];
//非常值得注意的事kSecValueData字段只接受UTF8格式的 NSData *类型,否则addItem/updateItem就会crash,并且一定记得带上service和account字段
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:passwordData forKey:(id)kSecValueData];
OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL);
[dictionary release];
if (status == errSecSuccess) {
return YES;
}
return NO;
}
- (BOOL)updateKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[updateDictionary setObject:passwordData forKey:(id)kSecValueData];
//这里也有需要注意的地方,searchDictionary为搜索条件,updateDictionary为需要更新的字典。这两个字典中一定不能有相同的key,否则就会更新失败
OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary,
(CFDictionaryRef)updateDictionary);
[searchDictionary release];
[updateDictionary release];
if (status == errSecSuccess) {
return YES;
}
return NO;
}
- (void)deleteKeychainValue:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
SecItemDelete((CFDictionaryRef)searchDictionary);
[searchDictionary release];
}
第二种 苹果提供的封装类 KeychainItemWrappe
使用时可能出现一些bug,需要注意,具体使用请参阅上述链接提供的demo
第三种 使用三方框架 SFHFKeychainUtils
使用简单
提供了四个方法用于增删改查
+ (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
+ (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
+ (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
+ (BOOL) purgeItemsForServiceName:(NSString *) serviceName error: (NSError **) error;
Keychain使用的注意事项
keychain的item有5种指定类型,分别是
extern CFTypeRef kSecClassGenericPassword
extern CFTypeRef kSecClassInternetPassword
extern CFTypeRef kSecClassCertificate
extern CFTypeRef kSecClassKey
extern CFTypeRef kSecClassIdentity OSX_AVAILABLE_STARTING(MAC_10_7, __IPHONE_2_0);
如果把keychain item的类型指定为需要保护的类型比如password,item的data会被加密并且保护起来,如果把类型指定为不需要保护的类型,比如certificates,item的data就不会被加密。
每种类型的Keychain item都有不同的键作为主要的Key也就是唯一标示符用于搜索,更新和删除,Keychain内部不允许添加重复的Item。
keychain item的类型,也就是kSecClass键的值 | 主要的Key |
---|---|
kSecClassGenericPassword | kSecAttrAccount,kSecAttrService |
kSecClassInternetPassword | kSecAttrAccount, kSecAttrSecurityDomain, kSecAttrServer, kSecAttrProtocol,kSecAttrAuthenticationType, kSecAttrPortkSecAttrPath |
kSecClassCertificate | kSecAttrCertificateType, kSecAttrIssuerkSecAttrSerialNumber |
kSecClassKey | kSecAttrApplicationLabel, kSecAttrApplicationTag, kSecAttrKeyType,kSecAttrKeySizeInBits, kSecAttrEffectiveKeySize |
kSecClassIdentity | kSecClassKey,kSecClassCertificate |
keychain的备份
iOS的Keychain由系统管理并且进行加密,Keychain内的信息会随着iPhone的数据一起备份。但是kSecAttrAccessible 属性被设置为后缀是ThisDeviceOnly的数据会被以硬件相关的密钥(key)加密。并且不会随着备份移动至其他设备。
kSecAttrAccessiblein变量用来指定这条信息的保护程度。我们需要对这个选项特别注意,并且使用最严格的选项。这个键(key)可以设置6种值。
CFTypeRef kSecAttrAccessibleWhenUnlocked;
CFTypeRef kSecAttrAccessibleAfterFirstUnlock;
CFTypeRef kSecAttrAccessibleAlways;
CFTypeRef kSecAttrAccessibleWhenUnlockedThisDeviceOnly;
CFTypeRef kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
CFTypeRef kSecAttrAccessibleAlwaysThisDeviceOnly;
从iOS5.0开始kSecAttrAccessible默认为kSecAttrAccessibleWhenUnlocked。
Keychain从iOS7.0开始支持iCloud备份。把kSecAttrSynchronizable属性设置为@YES,这样后Keychain就能被iCloud备份并且跨设备分享。
不过在添加kSecAttrSynchronizable属性后,这条属性会被作为每条Keychain Item的主要的Key之一,所以在搜索,更新,删除的时候如果查询字典内没有这一条属性,item就匹配不到。
网友评论