0x00 什么是Keychain

据介绍,Keychain是iOS系统官方提供的安全存储容器,我们可以用它来存一些敏感信息,EG:密码,用户名,证书等等。
Mac电脑上自带的Keychain Access.app就是苹果自己的Keychain读取软件。
Keychain是保存在沙盒之外的数据库的,所以在删除App后,在重新下载App后,这些信息依旧存在,并且你可以通过设置一些属性,让你的数据保#####存到iCloud中,达到跨设备存储。

0x01 Keychain本质是什么

Keychain存储本质是Sqlite。真机地址:/private/var/Keychains/keychain-2.db
既然是Sqlite,那么Keychain存储的对象(后面统称为Item),即对应Sqlite上的一条记录而已。

0x02 Item

Item 结构

  • Class -> kSecClass //指向对应的表结构
  • Attributes -> kSecAttrXXXX //每个表中对应的字段
  • Value -> kSecValueXXX //存储的类型

Class
有Sqlite,那么就会有对应的表。
Item在保存对象的时候,你需要指定你要保存的表。

1
2
3
4
5
extern const CFStringRef kSecClassGenericPassword //一般的密码存储表,基本用这个为主,对应genp表
extern const CFStringRef kSecClassInternetPassword //用来存储网络密码的表,对应inet表
extern const CFStringRef kSecClassCertificate //用来存储证书表,对应cert表
extern const CFStringRef kSecClassKey //存一些Key之类的东西,对应keys表
extern const CFStringRef kSecClassIdentity //一些特殊的认证信息之类的,这个不知道。

Attributes
关于Item的属性,对应的不同表是不同的。每个都介绍不现实,讲讲最常用的kSecClassGenericPassword表中的一些字段吧

属性 Value值 作用
kSecAttrAccessible kSecAttrAccessibleWhenUnlocked (Default) 你能获取到Keychain的数据必须在设备被解锁的情况下获取
kSecAttrAccessibleAfterFirstUnlock Keychain的数据在设备第一次解锁之后就能够获取使用
kSecAttrAccessibleAlways Keychain数据始终可以获取到
kSecAttrAccessGroup 自定义Like(@”AppIdentifierPrefix.com.hongzhi.test”) 设置你的存储区域,后面会详细讲
kSecAttrAccount 自定义 标识此条数据的主Key之一
kSecAttrService 自定义 标识此条数据的主Key之一
kSecAttrGeneric 自定义 非主键用来标识
kSecAttrSynchronizable BOOL 标识是否同步到iCloud

Note:
          kSecAttrAccessible还有另外三种是在上面这几个后面加ThisDeviceOnly,则数据不会跟随设备移动,会跟设备绑定。

          AppIdentifierPrefix是你开发证书的唯一标识符号,com.hongzhi.test是你应用的BundleId。

Value

属性 Value值 作用
kSecValueData Get或者Set 这个就是你要存数据的Value,只能存NSData对象

0x03 用法

既然是Sqlite数据库的存储,那么肯定就有增删该查。
每个函数都会有一个OSStatus的标识,标识操作是否成功,更多的标识参考SecBase.h文件中的OSStatus枚举值。

  1. SecItemAdd 添加一个keychain item
  2. SecItemUpdate 修改一个keychain item
  3. SecItemCopyMatching 搜索一个keychain item
  4. SecItemDelete 删除一个keychain item
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
- (NSMutableDictionary *)newSearchDictionary:(NSString *)identifier {
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
[searchDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
NSData *result = nil;
OSStatus status = SecItemCopyMatching((CFDictionaryRef)searchDictionary,
(CFTypeRef *)&result);
return result;
}
- (BOOL)createKeychainValue:(NSString *)password forIdentifier:(NSString *)identifier {
//设置添加的字典
NSMutableDictionary *dictionary = [self newSearchDictionary:identifier];
NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
[dictionary setObject:passwordData forKey:(id)kSecValueData];
OSStatus status = SecItemAdd((CFDictionaryRef)dictionary, NULL);
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];
OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary,
(CFDictionaryRef)updateDictionary);
if (status == errSecSuccess) {
return YES;
}
return NO;
}
- (void)deleteKeychainValue:(NSString *)identifier {
NSMutableDictionary *searchDictionary = [self newSearchDictionary:identifier];
SecItemDelete((CFDictionaryRef)searchDictionary);
}

0x04 关于iCloud备份

  1. 使用kSecAttrSynchronizable可以对存储的值进行跨设备的备份,但是添加这个字段后,搜索会以这个值作为主要的Key。
  2. 搜索匹配的时候要带上这个字段才能查询到相应的Item。

0x05 App间共享数据

  1. 关键东西:Keychain access group。
  2. 原理:Keychain通过Provisioning profile来区分应用,每个profile会带有相应的bundle id和添加的Access Group,应用保存的数据指定在对应的Access group中,如果没有在属性中设置,即存储在已bundle id命名的Access Group中。
  3. 非代码指定Access Group的话,就要在Capabilities中打开Keychain Sharing,在其中添加相应的BundleId,系统会自动帮你添加AppIdentifierPrefix,即Id值。
    Use:
    1
    [searchDictionary setObject:@“AppIdentifierPrefix.UC.testWriteKeychainSuit” forKey:(id)kSecAttrAccessGroup];

如果多个App指定了同一个Access group,并且它是由同一个证书下发的,那么这两个App就能够从这个Access Group中获取数据。

0xfe 总结

Keychain很方便,支持App间,设备间数据的传输,为一大群开发者解决了一堆的难题,但是也不能过度依赖于这个东西。Keychain的读取速度是NSUserDefault的3倍时长左右。一些简单的数据存储在Info.plist或者NSUserdefault中反而会更好。

0xff 参考链接

2013届WWDC Keychain简介
Max Blog Keychain